Nestjs-Test

25tutmmu·2022년 9월 21일
0

테스트의 종류

  • 유닛 테스트(Unit Testing) : 컴포넌트, 함수 단위의 작은 코드를 테스트
  • 엔드 투 엔드 테스트(End to End Testing) : 사용자 관점에서 취할만한 행동에 따라 각각의 서비스가 정해진 대로 동작하는지 테스트
  • 통합 테스트 : 각 모듈이 제대로 작성되었다면 이제 여러 모듈이 함께 동작했을 때 문제가 없는지 검증하는 과정
  • 부하 테스트(Load Testing), 스트레스 테스트(Stress Testing) : 얼마나 많은 동시 접속자를 처리할 수 있는지 테스트 (서버 관점에서의 테스트)

Nest.js에서는 기본적으로 testing tool은 jest를 사용하고 있으며, E2E(End To End) 테스트 파일을 만들어주고, 컨트롤러, 서비스 각각을 제너레이터를 사용하여 생성하면 유닛 테스트 파일도 자동으로 만들어줍니다.

아래의 원칙을 따를 경우 코드 품질이 훨씬 좋은 테스트 코드를 짤 수 있습니다.

FIRST 단위테스트 원칙

  • Fast — 유닛 테스트는 빨라야 한다.
  • Isolated — 다른 테스트에 종속적인 테스트는 절대로 작성하지 않는다. - 한 테스트와 다른 테스트가 상호작용을 하고 있으면 해당 테스트가 버그가 발생했을 때 어떤 부분에서 발생하였는지 파악이 어려우며 실제로 대부분의 단위테스트는 무작위순서로 실행되기 때문에 종속된 테스트의 결과에 영향을 미칠 수 있습니다.
  • Repeatable — 테스트는 실행할 때마다 같은 결과를 만들어야 한다. - 여러번 실행해도 다른 컴퓨터에서 실행되는 경우도 동일한 결과를 생성해야한다는 뜻입니다. Isolated(격리)가 잘되어있을수록 반복성이 좋다.
  • Self-validating — 테스트의 결과로 판단하는 것이 아니라 수동으로 테스트를 해서는 안된다. - 단위 테스트의 통과 여부를 알아보기 위해 개발자는 테스트가 완료된 후 추가 수동 검사를 수행해서는 안 됩니다.
  • Timely or Thorough— - 모든 사용 사례 시나리오를 다루도록 노력해야 합니다. - 테스트 주도 개발(TDD) 방법론에 적합한 원칙이지만 실제로 적용되지 않는 경우도 있다.
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from '../user/user.service';

describe('USerService', () => { // 테스트를 묘사..?
  let service: UserService;

  beforeEach(async () => { // 테스트하기전에 실행
    const module: TestingModule = await Test.createTestingModule({
      providers: [UserService],
    }).compile();

    service = module.get<UserService>(UserService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});
  • describe : 유사한 단위 테스트 사례를 그룹화하는 데 사용해야 합니다.
  • beforeEach : 각 테스트 케이스 전에 블록 내에서 코드를 실행하는 Jest 함수로 종속성을 주입하거나 test를 할 수 있도록 환경을 만드는데 사용됩니다. - compile()은 비동기로 실행되며 main.ts의 부트스트랩과정과 유사함
  • createTestingModule : 테스트 케이스에 필요한 모든 종속성을 가져오는 데 사용할 수 있는 TestingModule 인스턴스를 만듭니다.
  • it : 독립적인 단일 사용 사례라고 할 수 있으며 'describe'에는 여러 'it'이 있을 수 있습니다. - 성공했을경우, 404 409 등 에러의 경우 각각의 경우마다 it이 존재한다.
  • spyOn : spyOn은 함수가 원하는 출력을 반환하도록 강제하는 데 사용됩니다. 함수를 mocking하고 테스트에 사용하려는 결과를 반환합니다.
  • expect: 함수의 출력값이 기대하는 결과값과 일치하는지 검증하는 데 사용됩니다. 따라서 어떠한 메서드가 동일한 결과를 반환할 것으로 예상할 수 있습니다.

테스트 mocking

유닛테스트 시 mocking을 자주 사용합니다.
환경 구축을 위한 작업 시간이 많이 필요한 경우, 또는 테스트하려는 코드가 의존하는 부분을 직접 생성하기가 너무 부담스러울 때 Mock이 사용됩니다.

1.  mocking class - costom provider useClass이용하기

class MockService {
    findOne(id: string): Promise<User | null> {
        return null;
    }
    save(user: User): Promise<boolean> {
        return true;
    }
}

const moduleRef = await Test.createTestingModule({
  providers: [
    {
      provide: UserService,
      useClass: MockService,
    },
  ],
}).compile();

2. mocking object - costom provider useValue이용하기

const mockService = {
  findOne: (id: string): Promise<User | null> => {
    return null;
  },
  save: (user: User): Promise<boolean> => {
    return true;
  },
}.compile();

const moduleRef = await Test.createTestingModule({
  providers: [
    {
      provide: UserService,
      useValue: mockService,
    },
  ],
}).compile();

3. mocking function

it('should return true', async () => {
  userService.findOne = jest.fn().mockResolvedValue(true); //(1)
  jest.spyOn(userService, 'findOne').mockResolvedValue(true); //(2)
  expect(await userService.findOne()).toTruthy();
});
  • spyOn : spyOn은 함수가 원하는 출력을 반환하도록 강제하는 데 사용됩니다. 함수를 mocking하고 테스트에 사용하려는 결과를 반환합니다.

4. useMocker

import { UserService } from '../src/user/user.service';
...
const moduleRef = await Test.createTestingModule({
  providers: [
    UserService,
  ],
})
  .useMocker(token => {
  if (token === USerService) {
    return {
      findOne: jest.fn().mockResolvedValue(results)
    };
  }

레포지토리 mocking

type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;

Repository를 Mocking 하기위해 Repository Type을 정의한 것

Partial : 타입 T의 모든 요소를 optional하게 한다.
Record : 타입 T의 모든 K의 집합으로 타입을 만들어준다.
keyof Repository : Repository의 모든 method key를 불러온다.
jest.Mock : 3번의 key들을 다 가짜로 만들어준다.
type MockRepository<T = any> : 이를 type으로 정의해준다.


import * as request from 'supertest';
import { Test } from '@nestjs/testing';
import { CatsModule } from '../../src/cats/cats.module';
import { CatsService } from '../../src/cats/cats.service';
import { INestApplication } from '@nestjs/common';

describe('Cats', () => {
  let app: INestApplication;
  let catsService = { findAll: () => ['test'] };

  beforeAll(async () => {
    const moduleRef = await Test.createTestingModule({
      imports: [CatsModule],
    })
      .overrideProvider(CatsService)
      .useValue(catsService)
      .compile();

    app = moduleRef.createNestApplication();
    await app.init();
  });

  it(`/GET cats`, () => {
    return request(app.getHttpServer())
      .get('/cats')
      .expect(200)
      .expect({
        data: catsService.findAll(),
      });
  });

  afterAll(async () => {
    await app.close();
  });
});
  • createNestApplication(): Nest환경을 인스턴스화해주는 메소드
  • getHttpServer(): supertest 메소드로 사용하여 HTTP 요청을 한다.
  • afterAll : 데이터 베이스를 테스트 하기전으로 되돌리거나 실행을 종료할때 쓰인다.

테스트는 개발속도가 느리다는 단점이 있지만 리팩토링시 편리합니다. 현재 리팩토링 하고 있는 코드가 기존 시스템의 동작을 깨뜨리지 않을까하는 걱정을 하지 않아도 되며 잘 만든 테스트 코드는 소프트웨어의 품질을 높여줍니다. 또한 E2E테스트의 경우 CICD 과정에 테스트를 포함시키면 배포전 버그를 미리 파악할 수 있다는 장점이 있습니다.

0개의 댓글