Jest unit testing…

회사에서 개발하기 시작한 서버에서 본격적으로 유닛 테스트를 구현하는 방법에 대한 이해가 필요한 상황이다. 이에 공식 문서를 보면서 좀 정리를 해둘 필요를 느껴 정리해본다…

Getting Started

  • 설치
yarn add --dev jest
  • 특정 메서드를 생성한다.
// sum.js
function sum(a, b) {
  return a + b;
}
module.exports = sum;
  • 테스트 파일을 만들어 보자
// sum.test.js
const sum = require('./sum');

test(`adds 1 + 2 to equal 3`, () => {
  expect(sum(1, 2)).toBe(3);
})
  • 그 뒤 package.json 에 아래의 내용을 기록하면 된다.
{
  "scripts": {
    "test": "jest"
  }
}
  • 이제는 npm 혹은 yarn 을 이용해 jest 테스트를 실행할 수 있다. 그리고 Jest는 다음과 같은 메시지를 띄울 것이다.
> yarn test 
...
PASS ./sum.test.js  
✓ adds 1 + 2 to equal 3 (5ms)

Test with NestJS

  • 자동화된 테스트는 어떤 진지한 소프트웨어 개발의 필수적인 영역이다.
  • 자동화는 개별적인 테스트의 반복을 쉽게 만들어주고, 개발 과정 동안 테스트를 빠르고 쉽게 하도록 도와준다.
  • 자동화는 개별 개발자들의 생산성을 증대시키며, 소스 코드 제어 체크인, 기능 통합 및 버전 릴리스와 같은 중요한 개발 수명 주기 시점에서 테스트가 실행되도록 보장한다.
  • 이러한 점에서 Nest 프레임워크는 효과적인 테스트를 포함한 개발을 장려하고자 노력하고 있으며, Nest에 다음과 같은 기능을 포함하고 있다.
    • 구성요소에 대한 기본 단위 테스트와 애플리케이션에 대한 e2e 테스트를 자동으로 스캐폴드 한다.
    • (독립된 모듈/ 어플리케이션 로더를 빌드하는 테스트 러너 등)기본적인 테스팅 툴을 제공한다.
    • 테스트 도구에 구애 받지 않으면서, 즉시 사용 가능한 Jest, Supertest 와의 통합을 제공한다.
    • 쉽게 구성요소를 모의할 수 있도록 테스트 환경에서 Nest 종속성 주이비 시스템을 사용할수 있다.
  • Nest는 기본적으로 특정 도구를 강제하지 않아, 테스트 프레임워크를 원하는 것으로 교체하는 것도 가능하다. 테스트기의 교체만으로도 기본적인 Nest의 구조나 기능은 그대로 사용하면서 테스트 프레임워크의 이점을 접목시킬 수 있다.

Unit Testing

  • CatsControllerCatsService에 대한 테스트 코드를 보자. Jest 기반이다.
  • test-runner 를 제공하고, assert 함수와, test-double 유틸리티 등을 통해 모킹, 스파잉 등의 테스트 기술을 보여준다.
  • 아래의 기본 테스트에서는 이러한 클래스를 수동으로 인스턴스화 하고 컨트롤러와 서비스가 API 기준에 맞춰 이행하는지를 검사하는 테스트를 보여준다.
// cats.controller.spec.ts 
import { CatsController } from './cats.controller';
import { catsService } from './cats.service';

describe('CatsController', () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(() => {
    catsService = new CatsService();
    catsController = new CatsController(catsService);
  });

  describe('findAll', () => {
    it('should return an array of cats', async () => {
      const result = ['test']
      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

      expect(await catsController.findAll().toBe(result);
    });
  });
});

Testing utilities

  • @nestjs/testing 패키지는 보다 로버스트한 테스팅 프로세스를 가능케 하는 유틸리티들을 제공한다. 이를 활용하여 위의 예시를 수정해보자.
// cats.controller.spec.ts 
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { catsService } from './cats.service';

describe('CatsController', () => {
  let catsController: CatsController;
  let catsService: CatsService;

  beforeEach(() => {
    const moduleRef = await Test.createTestingModule({
        controllers: [CatsController],
        providers: [CatsService],
      }).compile();

    catsService = moduleRef.get<CatsService>(CatsService);
    catsController = moduleRef.get<CatsController>(CatsController);
  });

  describe('findAll', () => {
    it('should return an array of cats', async () => {
      const result = ['test']
      jest.spyOn(catsService, 'findAll').mockImplementation(() => result);

      expect(await catsController.findAll().toBe(result);
    });
  });
});
  • Test 클래스는 기본적으로 전체 Nest 런타임을 모의하는 애플리케이션 실행 컨텍스트를 제공하는 데 유용하지만, 모의 및 재정의를 포함하여 클래스 인스턴스를 쉽게 관리할 수 있는 후크를 제공한다.
  • Test 클래스에는 모듈 메타데이터 객체(@Module() 데코레이터에 전달하는 것과 동일한 객체)를 인수로 사용하는 createTestingModule() 메서드가 있다. 이 메소드는 몇 가지 메소드를 제공하는 TestingModule 인스턴스를 반환한다.
  • 단위 테스트에서 중요한 것은 compile() 메서드다. 이 메서드는 종속성이 있는 모듈을 부트스트랩해주고, 테스트할 준비가 된 모듈을 반환한다.

compile() 메소드 해당 메소드는 비동기 식이므로 기다려야 하고, 모듈이 컴파일 되면 get() 메서드를 사용하여 선언된 정적 인스턴스(컨트롤러 및 프로바이더)를 검색할 수 있다.

  • TestingModule 은 모듈 참조 클래스에서 상속되므로 지정된 공급자를 동적으로 확인하는 기능이 있다. resolve() 메서드를 통해 이 작업이 수행 가능하다.(get() 메서드는 정적 인스턴스만 얻어낼 수 있다.)
const moduleRef = await Test.createTestingModule({
	controllers: [CatsController],
	providers: [CatsService],
}).compile();

catsService = await moduleRef.resolve(CatsService);
  • (주의) : resolve() 메서드를 통해 반환받은 프로바이더의 인스턴스는 그것의 의존성 주입 컨테이너 서브 트리로부터의 유니크한 객체이다. 각 서브트리는 고유한 컨텍스트 구분자를 가지고 있다. 그래서 해당 메서드를 한 번 이상 호출한다면, 각 참조된 인스턴스들은 모두 다른 대상이다. -> 결국 이 말은 해당 테스팅에 사용되도록 resolve 메서드를 통해 구체화된 인스턴스들은 해당 유닛테스트에 독립적으로 존재하는 인스턴스라는 말로 해석된다.
  • 프로덕션 버전의 프로바이더를 사용하는 대신, 테스트를 목적으로 하는 커스텀 프로바이더로 오버라이드를 하는 것이 가능해진다. 이를 활용하면 예를 들어 라이브로 사용중인 데이터 베이스를 사용하는 게 아니라, 데이터 베이스 서비스를 대신 사용하는 것이 가능하다.

Auto mocking

  • Nest는 또한 누락된 모든 종속 항목에 적용 가능한 모킹 팩토리를 정의할 수 있다. 이는 매우 유용하여서, 클래스 안에 대량의 의존성을 가지거나, 전체를 전부 모킹하는 것에 긴 시간이 소요되는 경우 매우 유용하다.
  • 이를 위해서는 createTestingModule()useMocker() 메서드와 연결하여 종속성 모의 객체에 대한 팩토리를 전달 해야한다.
  • 이 팩토리는 인스턴스 토큰인 선택적 토큰, Nest 프로바이더에게 유효한 모든 토큰을 가져와 모의로 구현이 가능하다.
  • 아래는 jest-mock을 사용하여 일반적인 mocker 를 생성하거 jest.fn() 을 사용하여 CatsService 에 대한 특정 Mocker 를 생성하는 예시를 보여준다.
//...
import { ModuleMocker, MockFunctionMetadata } from 'jest-mock';

const moduleMocker = new ModuleMocker(global);

describe('CatsController', () => {
  let controller: CatsController;

  beforeEacj(async () => {
    const moduleRef = await Test.createTestingModule({
      controllers: [CatsController],
	})
	  .useMocker((token) => {
	    const results = ['test1', 'test2'];
	    if (token === CatsService) {
	      return { findAll: jest.fn().mockResolvedValue(results) };
	    }
	    if (typeof token === 'function') {
		    const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadaa<any, any>
		     const Mock = moduleMocker.generateFromMetadata(mockMetdata);
		     return new Mock();
	    }
	  })
	  .compile();

    controller = moudlRef.get(corntroller);
  });
});