Logo

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가 생성해준 프로젝트를 열어보면 AppControllerAppService 클래스가 아미 작성되어 있을텐데요. 이대로 테스트하기에는 구현이 너무 간단해서 살짝 수정해주도록 하겠습니다.

src/app.controller.ts
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) };
  }
}
src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
  getHello(name: string) {
    return `Hello, ${name}!`;
  }
}

AppModule 클래스는 통합 테스트를 할 때 필요하며 변경없이 그대로 사용 가능합니다.

src/app.module.ts
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/testingsupertest 패키지를 실습 프로젝트에 개발 의존성으로 설치하겠습니다.

$ 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() 함수만 호출하여 인스턴스를 얻을 수가 있는 것이지요.

src/app.controller.spec.ts
import { Test } from '@nestjs/testing';import { AppController } from './app.controller';
import { AppService } from './app.service';

describe('AppController', () => {
  let controller;
  let service;

  beforeEach(async () => {
    const 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).toHaveBeenCalledTimes(1);
      expect(service.getHello).toHaveBeenCalledWith('World');
    });

    it('returns a personalized message', () => {
      jest.spyOn(service, 'getHello').mockReturnValue('Hey, John~');
      expect(controller.hello('John')).toEqual({ message: 'Hey, John~' });
      expect(service.getHello).toHaveBeenCalledTimes(1);
      expect(service.getHello).toHaveBeenCalledWith('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를 호출할 수 있도록 해줍니다.

test/app.e2e-spec.ts
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에 관련된 다른 글은 관련 태그를 참고 바라겠습니다.