Nestjs에서 Jest사용

Posted by surin01 on January 16, 2022 · 9 mins read

Jest?

Jest is a delightful JavaScript Testing Framework with a focus on simplicity.

It works with projects using: Babel, TypeScript, Node, React, Angular, Vue and more!

(jestjs.io)[https://jestjs.io/] 뭐… 웹사이트에서는 간단함에 중점을 맞춘 테스팅 프레임워크라고 한다. 직접 사용해보니까 그렇게 간단하지만은 않은것 같은데…. 아직 미숙해서 그런걸까

암튼 javascript로 동작하는 많은 프레임워크들과 함께 사용할 수 있다. 그리고 nest에서 jest를 사용하기 위한 세팅은 매우 간단하고, nest 레포를 init하게 되면 자동으로 app.controller에 대한 jest용 파일이 있을정도로 권장하고 있다.

기존 mock을 만들고 테스트하고 하는데 다른 프레임워크들을 사용했지만, jest를 사용하게 되면 거의 모든 경우를 jest 하나로 테스트 할 수 있다.

그럼 jest로 뭘 테스트하느냐? 단위테스트를 시행하고 하나의 기능이 올바르게 동작하는지를 독립적으로 테스트하는 것이다. 하나의 모듈을 기준으로 독립적으로 진행되는 가장 단위의 테스트이기 때문에, 하나의 기능, 혹은 메소드에 대한 테스트로 이해할 수 있겠다.

단위 테스트는 통합 테스트에서 필요로 하는 데이터베이스, 캐시 등과 실제 연결에 들이는 비용을 줄일 수 있고, 시스템을 구성하는 컴포넌트들이 많아질수록 테스트를 수행하는데 들이는 비용이 커지게 된다. 단위 테스트는 해당 부분만 독립적으로 테스트하기 때문에 어떤 코드를 생성하고, 리팩토링 할 때 빠르게 문제 여부를 확인하고 미리 대처할 수 있는 장점이 있다.

또한 단위테스트를 미리 작성하고 로직을 작성할 때, 단위 테스트 코드는 그 자체로 코드가 어떠한 방향을 가지고 인풋과 아웃풋을 가져야 하는지에 대한 명세가 될 수 있기 때문에, 단위 테스트를 수행하는 것이 좋다고 설명한다.

그럼 우리도 이제 단위 테스트에 발을 한번 담궈보자.

nestjs에서 jest를 사용할 것 이니, 설치부터 진행한다.

 npm i --save-dev @nestjs/testing

설치가 끝났다면, 우선 테스트용 스크립트를 작성하고 테스트까지 수행해 보도록 하자.

Jest unit test

import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

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

  beforeEach(async () => {
    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);
    });
  });
});

기본적인 controller를 테스트하는 jest 코드다. 하나의 기능에 대해서 it으로 테스트를 진행하고, expect를 통해서 값을 받은 후 toBe, 혹은 toEqual을 가지고 예상 값과 비교를 한 후 결과를 반환한다.

위에서 컨트롤러와 프로바이더 인스턴스를 생성할 때, test클래스의 createTestingModule 메소드를 사용해서 생성하게 되는데, 이는 nestjs런타임 상에서의 실행 컨텍스트를 제공하고 모킹 맻 재정의를 포함하여 클래스 인스턴스를 쉽게 관리할 수 있는 후크를 제공한다. 이는 기존의 모듈보다 몇가지 더 많은 메소드를 제공하는 TestingModule을 반환하고, compile() 메소드를 통해서 의존성이 있는 모듈을 부트스트랩하고 테스트 준비가 된 모듈을 반한하게 된다.

Jest관련 테스팅 명령어

테스트에 자주 사용되는 명령어들이다. 기본적인 코드에서 보이는 toBe()말고도 유용한 명령어들이 많고, 특히 toEqual은 오브젝트를 비교할때 자주 쓰이는 메소드이기 때문에 잘 알아놓으면 좋아보인다.

expect().toBe(a) // 예상한 값이 매개변수와 같은 값일 것인지 확인
expect().toEqual(obj) // 매개변수(객체)와 같은 값일 것이라 예상 객체가 가진 값의 비교가 가능
expect().not.toBe(a) // 뒤의 결과를 부정하는 값과 비교

expect().toBeNull() // 예상한 값이 null 인지 확인
expect().toBeUndefined() // 예상한 값이 undefined 인지 확인
expect().toBeDefined() // 예상한 값이 undefined 가 아닌지 확인
expect().toBeTruthy() // 예상한 값이 truthy 한 값인지 확인
expect().toBeFalsy() // 예상한 값이 falsy 한 값인지 확인

expect().toBeGreaterThan(number); // number보다 큰 값인지 확인
expect().toBeGreaterThanOrEqual(number); // number보다 크거나 같은 값인지 확인
expect().toBeLessThan(number); // number보다 작은 값인지 확인
expect().toBeLessThanOrEqual(number); // number보다 작거나 같은 값인지 확인
expect().toBeCloseTo(float) // float인 매개변수와 같은 값인지 확인 부동소수점 에러를 해결하기 위해 고안됨

expect().toMatch(string) // string을 포함하는 문자열인지 확인
expect().toContain('item') // item을 포함하는 배열(iterator)인지 확인

expect().toThrow() // 예외를 발생시키는지 확인

E2E Test

기본적으로 모듈들이 잘 동작되는지 테스트를 진행했다. 이제 실제 최종 사용가자 제품에서 가질 수 있는 상호 작용의 종류에 더 가까운 쳉험을 테스트해보자. End to End, 줄여서 E2E 테스트라고 부르는 이 테스트는 총체적인 수준에서 클래스 및 모듈의 상호 작용을 다룬다. Api의 엔드포인트가 많아질수록 동작을 수동으로 테스트하기가 어려워지기 때문에 자동화된 종단 테스트는 시스템의 전반적인 동작이 정확하고 프로젝트 요구사항을 충족하는지 확인하는데 도움을 줄 수 있다.

e2e테스트는 방금 확인한 단위 테스트와 비슷한 구성을 사용하지만, Jest가 아니라 Supertest라는 라이브러리를 사용하여 HTTPㅇ청을 쉽게 시뮬레이션 할 수 있다.

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();
  });
});

앞서 유닛 테스트에서 compile()메소드를 통해서 테스트할 모듈 인스턴스를 생성한 것처럼, 여기서는 createNestApplication()메소드를 사용하여 전체 Nest 런타임 환경을 인스턴스화 한다. 실행중인 앱에 대한 참조를 app변수에 저장하여 HTTP요청을 시뮬레이션하는데 사용할 수 있다.

Upsertest의 request()함수를 사용하여 Http테스트를 진행한다. request()안의 app.getHttpServer()를 가지고 리매핑된 Http 서버를 전달하고, 이 서버는 실ㄹ제 http 요처을 시뮬레이션ㄴ하는 메소드를 노출한다. 예를 들어 위 예제의 .get(“/cats”) 메소드는 네트워크를 통해 들어오는 실제 http 요청 get /cats 와 동일한 요청이 시작된다.

결론

단위 테스트와 e2e테스트를 활용하면 예측할 수 있는 버그들에 대해 예방하고 코드의 품질을 높일 수 있게 된다. 내가 생각했을때 완벽하다고 생각한 경우에도 버그는 일어나는게 다반사니… 다음 프로젝트에서는 단위 테스트를 작성하고, 그에 대한 비즈니스 로직을 생성하는 쪽으로 설계하고 구현하는 것도 재밌을것 같다!