Jest와 Supertest 활용한 NestJS 테스트
유지보수가 용이하고 안정적으로 동작하는 NestJS API를 개발하려면 각각의 엔드포인트가 잘 작동하는지 확인하는 것이 필수적입니다.
이번 글에서는 Jest와 Supertest를 활용하여 효과적으로 NestJS 앱을 테스트하는 방법에 대해서 알아보겠습니다.
실습 프로젝트 구성
먼저 간단한 실습을 위해서 NestJS 프로젝트가 하나 필요할 것 같은데요.
터미널에서 NestJS CLI 도구의 nest new
명령어를 실행하여 새로운 프로젝트를 구성하도록 하겠습니다.
$ nest new our-nestjs
⚡ We will scaffold your app in a few seconds..
? Which package manager would you ❤️ to use? (Use arrow keys)
❯ npm
yarn
pnpm
NestJS CLI를 설치하고 NestJS 프로젝트를 구성하는 기본적인 방법은 관련 포스팅을 참고 바랍니다.
테스트 대상 코드
NestJS CLI가 생성해준 프로젝트를 열어보면 AppController
와 AppService
클래스가 아미 작성되어 있을텐데요.
이대로 테스트하기에는 구현이 너무 간단해서 살짝 수정해주도록 하겠습니다.
import { Controller, Get, Query } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('hello')
hello(@Query('name') name: string = 'World') {
return { message: this.appService.getHello(name) };
}
}
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(name: string) {
return `Hello, ${name}!`;
}
}
AppModule
클래스는 통합 테스트를 할 때 필요하며 변경없이 그대로 사용 가능합니다.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
패키지 설치
우선 npm을 이용하여 @nestjs/testing
와 supertest
패키지를 실습 프로젝트에 개발 의존성으로 설치하겠습니다.
$ npm i -D @nestjs/testing @types/supertest supertest
@nestjs/testing
패키지는 NestJS에서 테스트 코드를 작성할 때 유용한 다양한 유틸리티를 포함하고 있습니다.
supertest
패키지는 REST API에 대한 통합 테스트를 좀 더 간단하게 작성할 수 있도록 도와주는 라이브러리입니다.
@types/supertest
패키지는 supertest
라이브러리에 대한 타입 정보를 담고 있습니다.
단위 테스트 작성
먼저 AppController
클래스가 독립적으로 잘 작동하는지 검증하기 위해서 단위 테스트를 작성해볼까요?
AppController
클래스는 내부적으로 AppService
클래스에 의존하고 있는데요.
NestJS의 테스팅 유틸리티가 우리 대신에 이 클래스들의 인스턴스를 생성하고 알아서 의존성 주입을 해줄 수 있습니다.
따라서 @nestjs/testing
에서 Test
를 불러온 후 createTestingModule()
함수에 이 두 클래스를 모두 인자로 넘겨줍니다.
그러면 테스트 용으로 사용할 수 있는 NestJS 모듈이 구성되며 우리는 편하게 get()
함수만 호출하여 인스턴스를 얻을 수가 있는 것이지요.
import { Test } from '@nestjs/testing';import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let controller;
let service;
beforeEach(async () => {
let module = await Test.createTestingModule({ controllers: [AppController], providers: [AppService], }).compile();
controller = module.get(AppController); service = module.get(AppService); });
describe('hello', () => {
it('returns a default message', () => {
jest.spyOn(service, 'getHello').mockReturnValue('Hey~');
expect(controller.hello()).toEqual({ message: 'Hey~' });
expect(service.getHello).toBeCalledTimes(1);
expect(service.getHello).toBeCalledWith('World');
});
it('returns a personalize message', () => {
jest.spyOn(service, 'getHello').mockReturnValue('Hey, John~');
expect(controller.hello('John')).toEqual({ message: 'Hey, John~' });
expect(service.getHello).toBeCalledTimes(1);
expect(service.getHello).toBeCalledWith('John');
});
});
});
단위 테스트를 작성할 때는 모킹(mocking)을 통해 테스트하려는 클래스를 적절히 격리시켜주는 것이 중요합니다.
예제 테스트에서는 Jest의 spyOn()
함수를 호출하여 AppService
클래스의 getHello()
메서드가 적절한 문자열을 반환하도록 모킹하고 있습니다.
Jest에서
fn()
이나spyOn()
을 사용하여 함수를 모킹하는 방법에 대해서 별도 글에서 자세히 다루고 있으니 참고 바랍니다.
이제 작성한 단위 테스트를 실행해보면 다음과 같이 모두 통과하는 것을 볼 수 있습니다.
$ npm test app.controller
$ jest
PASS src/app.controller.spec.ts
AppController
hello
✓ returns a default message (648 ms)
✓ returns a personalize message (643 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 4.099 s
Ran all test suites.
통합 테스트 작성
AppController
클래스에 구현되어 있는 GET /hello
API 앤드포인트가 예상대로 동작하는지 확인하기 위해서 통합(integration, e2e) 테스트를 작성해보겠습니다.
통합 테스트를 작성할 때는 createTestingModule()
함수에 단위 테스트를 작성할 때처럼 각각의 클래스를 넘기는 것이 아니라 AppModule
을 통째로 넘겨서 테스팅 모듈을 구성합니다.
그리고 createNestApplication()
함수를 통해 NestJS 앱을 구동하여 REST API를 호출할 수 있도록 해줍니다.
import * as request from 'supertest';import { Test } from '@nestjs/testing';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app;
beforeAll(async () => {
const module = await Test.createTestingModule({
imports: [AppModule], }).compile();
app = module.createNestApplication(); await app.init(); });
afterAll(async () => {
await app.close();
});
it('GET /', () => {
return request(app.getHttpServer())
.get('/')
.expect(404)
.expect({
error: 'Not Found',
message: 'Cannot GET /',
statusCode: 404,
});
});
it('GET /hello', async () => {
return request(app.getHttpServer())
.get('/hello')
.expect(200)
.query({ name: 'Dale' })
.expect({
message: 'Hello, Dale!',
});
});
});
supertest
에서 불러온 request
함수를 통해서 매우 간단한 문법으로 앤드포인트를 호출하고 응답 결과를 검증할 수 있습니다.
GET /
는 별도로 구현하지 않았기 때문에 404
가 응답되는지만 추가로 테스트해보았습니다.
마찬가지로 통합 테스트도 실행해보면 두 개의 테스트가 모두 통과하는 것을 볼 수 있습니다.
$ npm run test:e2e app.e2e-spec
$ jest --config ./test/jest-e2e.json
PASS test/app.e2e-spec.ts (5.865 s)
AppController (e2e)
✓ GET / (880 ms)
✓ GET /hello (852 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 5.927 s, estimated 6 s
Ran all test suites.
참고로 본 예제에서는 통합 테스트는 별도로 모아서 test
폴더에서 두는 NestJS의 관행을 따르고 있습니다.
전체 코드
실습 프로젝트에서 작성한 테스트 코드는 아래에서 직접 확인하고 실행해볼 수 있습니다.
마치면서
이번 글에서는 NestJS에서 테스트를 어떻게 작성할 수 있는지에 대해서 자세히 다루어 보았습니다. NestJS를 사용하는 개발자라면 이번 글에서 다룬 Jest와 Supertest를 이용한 테스트 방법을 통해 더 나은 개발 경험을 하실 수 있었으면 좋겠습니다.
NestJS에 관련된 다른 글은 관련 태그를 참고 바라겠습니다.