🧐
Jest 테스터 코드 작성하기 (1)
June 16, 2024
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
CatsController
와CatsService
에 대한 테스트 코드를 보자. 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);
});
});