Dev/SW Engineering

[TDD] TDD 시작하기 (Unit Test, Jest)

HJChung 2021. 2. 22. 20:32

Unit Test

유닛 테스트란 프로그래밍 후 소스 코드의 프로그램의 기본 단위인 모듈이 의도대로 정확히 작동하는지 검증하는 절차이다. 즉, 소스 코드의 개별 단위를 테스트하여 사용할 준비가 되었는지 확인하는 테스트 방법이다. 

 

1) 유닛 테스트를 하는 이유

  1. 프로그램이 커서 메모리가 많이 들고 다른 리소스(데이터베이스 등)이 막 필요한 경우 로컬 환경에서 코드를 실행시켜보기도 어렵고, 그걸 매번 수동으로 QA해보기도 어렵다. 그래서 유닛 테스트를 만들어서 빠르게 자신의 코드가 정상적으로 작동 하는지 확인 하는 것이 좋다. 
  2. 종속성이 있는 다른 클래스들에서 버그가 나는 것을 방지하기 위해서이다.
    • 예를 들어, A Class에서 버그가 나는데, 그것이 A class 내의 Common class 에서 발생하는 에러였다. 
    • 그래서 Common class의 에러 나는 부분을 수정하고, A class의 버그가 사라졌다. 그러나
    • Common class는 C class에서도 사용 중이었다. 그랬더니 Common class를 수정 후 원래는 잘 작동하던 C class에서 버그가 생겨버렸다. 
    • 그래서 C class에서 발생하는 버그를 잡기 위해서 Common class를 다시 수정 해야 하고... 이러면 A class에서 다시 에러가 발생한 가능성이 높다. 
    • 하지만 이러한 상황에서 유닛 테스트를 한다면 Common class에 의존하는 다른 클래스드로 확인 가능하기 때문에 해당 에러를 잡기 더 쉬워진다. 

하지만 프로그래밍 된 것은 수많은 모듈들이 정보를 주고 받으며 연결되어 있고, 어떤 경우 한 모듈을 테스트하려면 그 모듈과 연관된 상위, 하위 모듈 역시 잘 작동해야 해당 모듈을 정확히 테스트 할 수 있다. 그러나 상, 하위 모듈이 준비가 되어있지 않을 수도 있으므로 실제 상위 모듈 대신 가상의 역할을 하는 테스트 드라이버, 하위 모듈의 역할을 하는 테스트 스텁 을 사용하기도 한다.

 

2)  유닛 테스트를 하는 것의 장점

  1. 작은 각 단위가 잘 작동하는지 그때 그때 테스트해봄으로써 문제점을 빨리 발견하고 대응할 수 있으며, 그렇기 때문에 프로그래밍의 안정성이 높아질 뿐만 아니라 길게 보면 오히려 디버깅 시간을 단축시켜 개발 시간도 단축시킬 수 있다.
  2. 유닛 테스트를 통해서 기능은 바뀌지 않지만 코드를 정리해나가는 등의 리팩토링 작업을 할 때, 기존의 유닛 테스트를 통해 의도한 기능 여부를 알 수 있다. 즉, 지속적인 유닛 테스트 환경을 구축하면 어떠한 변화가 있더라도 코드와 그 실행이 의도대로 잘 되었는지를 확인, 검증 할 수 있다.
  3. 먼저 프로그램의 각 단위 부분을 검증하였기에 그에 대한 확신을 가질 수 있다. 그래서 그 단위들을 합쳐서 다시 검증하는 통합 테스트에서도 짐을 덜 수 있다.

3) 유닛 테스트의 조건

  1. 독립적이어야 하며, 어떤 테스트도 다른 테스트에 의존하지 않아야 한다.
  2. 그래서 Ajax, Axios, LocalStorage등 테스트 대상이 의존하는 것들을 대체하여서 딱 테스트하고 싶은 것만 격리 되어야 한다. 

Jest

Jest란

Facebook에서 만든 테스팅 프레임 워크이다. 유닛 테스트를 학 위해 test case를 만들어서 코드가 의도대로 잘 돌아간는지 확읺할 수 있다. 

※ 원래 mocha를 배운 적이 있는데, 크게 차이점은 없는 것 같고, mocha, should, supertest에 대한 내용은 해당 블로그에 잘 정리 되어 있는 듯 하다. 모카, 슈드, 슈퍼테스트에 대한 내용을 정리 해 둔 글

 

1) Jest 사용방법

  1. Jest 라이브러리 설치 npm install jest --save-dev (개발환경에서 test를 하는 것이므로 --save-dev로 설치해준다.)
  2. package.json에서 test script 변경 "scripts": {"test": "jest"} 또는 "scripts": {"test": "jest --watchAll"}. 이렇게 script를 작성해주면 npm test 할 때 jest가 알아서 test 파일을 찾아서 test를 진행한다. 
  3. 테스트를 작성할 폴더 및 파일 기본 구조 생성
    • test 폴더
      • 단위 테스트를 위한 unit 폴더
        • 단위 테스트 파일 [대상 이름].test.js
      • 통한 테스트를 위한 integration 폴더
        • 통합 테스트 파일 [대상 이름].test.init.js

2) Jest 파일 구조

Jest의 파일 구조는 describe안에 test case들을 하나씩 넣는 것이다. describe는 여러 관련 테스트를 그룹화(블록) 하는 것이고, it(test)란 개별 테스트를 수행하는 것으로 각 테스트를 설명하고, 작성하는 것이다. 

이 test는 expect와 matcher로 이루어지는데, expect 함수는 값을 테스트할 때 마다 사용되는 주어진 입력값에 대해서 리턴하는 값이 기대한는 값과 같은지를 비교하는데 사용된다.  matcher는 여러 방법으로 test하도록 사용되는 것이며 expect에서 기대조건에 해당하는 함수를 말한다. 

3) jest.fn()이란

Mock 함수를 생성하는 함수이다. 이 Mock 함수가 하는 일은 단위 테스트를 작성 할 때 해당 코드가 의존하는 부분을 가짜로 대체해준다. 

 

예를 들어서 데이터베이스에 데이터를 저장하는 코드에 대해서 유닛 테스트를 할 경우,

Mock 함수 없이 실제 사용 중인 데이터베이스로 테스트를 진행한다면, 데이터를 전송하는 중에

- 데이터베이스 접속작업이 직접 이루어지므로 테스트 실행 속도 저하

- 테스트 코드보다 데이터베이스와 연결을 맺고 트랜잭션을 생성하고, 쿼리를 전송하는 코드가 더 길어질 수 있다.

- 만약 테스트 실행 순간 일시적으로 데이터베이스가 죽어 있었다면 해당 테스트는 코드 문제가 아니었는데도 실패

- 테스트 종료 후 데이터베이스에서 변경 데이터를 직접 원복하거나 트랜잭션을 rollback해줘야 하는 경우가 있음

등의 고려해야할 것이 너무 많다. 

 

특히 더 문제인 것은 특정 기능만 고립하여서 독립적으로 test 해보아야 하는 유닛 테스트 조건과 맞지 않게 의존적인 부분이 생겨 영향을 받게 된다는 것이다. 

 

그래서 Mock함수인 jest.fn()이 필요하다. 또한 jest.fn()이 생성한 가짜 함수는 이 함수에 어떤 일들이 발생했는지, 다른 코드들에 의해서 어떻게 호출되는지를 기억하기 때문ㅇ에 이 함수가 내부적으로 어떻게 사용되는지 검증하기도 쉽다. 

 

4) jest.fn() 사용법

  1. Mock 함수 생성 const mockFunction = jest.fn()
  2. Mock함수 호출 (인자를 넘기는 것도 가능) mockFunction([인자])
  3. mockReturnValue 메소드를 이용해서 Mock 함수가 어떠한 결과값을 반환 할 지 직접 알려주기 mockFunction.mockReturnValue('가짜 함수 반환')
  4. toBeCalledTimes를 이용해서 Mock함수가 몇 번 호출되었고, toBeCalledWith를 이용해서 어떤 인자가 넘어왔는지 검증이 가능하다. 

TDD

1) TDD 진행 순서

  1. 해야 할 일이 무엇인지 생각하기
  2. 유닛 테스트 작성
  3. 테스트에 대응하는 실제 코드 작성

예를 들어, products 테이블에 product 데이터를 하나 생성하는 create 작업에 대해 TDD를 진행한다고 해보자. 그러면 

  1. 해야 할 일은? 데이터베이스 products 테이블에 product를 저장하는 것이므로 product를 저장하기 위한 함수를 먼저 생성한다. 
  2. 유닛 테스트 작성 
// test/unit/product.test.js
describe('Product Controller Create', () => {
  it('should have a createProduct function', () => {
    //product 데이터 생성을 위한 함수가 있는지 여부를 확인하기 위한 테스트 코드
    expect(typeof productController.createProduct).toBe('function');
  });
});

     3. 테스트에 대응하는 실제 코드 작성

// controller/products.js
const productModel = require('../models/Product');

exports.createProduct = async (req, res, next) => {
 
};

2) Mock함수를 사용한 TDD 진행 순서

예를 들어, products 테이블에 product 데이터를 하나 생성하는 create 작업에 대해 TDD를 진행한다고 해보자. 그러면

  1. 해야 할 일은? createProduct 함수를 호출할 때 Product Model의 Create 메소드가 호출이 되는지를 확인
  2. 유닛 테스트 작성
  it('should call Product.create', () => {
    //createProduct 함수를 호출할 때 Product Model의 Create 메소드가 호출이 되는지를 확인 해주기 위한 테스트 코드
    productController.createProduct(); //productController.createProduct이 호출 될 때,
    expect(productModel.create).toBeCalled(); //productModel.create가 호출되는지
    //그런데 이 테스트에서는 productModel에 의존적이어서는 안된다. 그래서 Mock함수를 사용한다. 
  })

 

여기서 productModel이라는 데이터베이스에는 테스트 코드가 의존적이면 안된다. 그래서 jest.fn()을 이용해서 Mock함수를 생성해야 한다. 

//Mock함수 생성
productModel.create = jest.fn();

 

     3. 테스트에 대응하는 실제 코드 작성

const productModel = require('../models/Product');

exports.createProduct = async (req, res, next) => {
  productModel.create();
};

 

reference

따라하며 배우는 TDD 개발

Jest를 이용한 Unit Test 적용기

테스트 프레임워크 🛠 ▻ Jest 4탄