[회고] 신입 iOS 개발자가 되기까지 feat. 카카오 자세히보기

🛠 기타/WEB

Nest.js - unit testing, e2e testing (feat. jest)

inu 2021. 1. 8. 15:27

UNIT TESTING vs E2E TESTING

  • UNIT TESTING : 프로젝트에서 각각의 function을 따로 테스트하는 것이다. 각각의 function이 제대로 기능을 수행하는지 확인하기 위한 테스트이다. (ex. 영화목록을 모두 보여주는 함수, 하나 보여주는 함수, 영화목록 중 일부를 삭제하는 함수 등이 각각 잘 수행되는지)
  • E2E TESTING : 이름 그대로(End-to-End testing) 모든 시스템을 테스팅하는 것이다. 사용자의 입장에서 페이지 간의 연결성 등을 테스트할 때 사용한다. (ex. 사용자가 특정 링크를 클릭했을 때 연결된 페이지를 정상적으로 볼 수 있어야 함)
  • 해당 게시글에서는 Nest.js 프로젝트에서 jest를 이용해 unit testing을 진행하는 방법을 알아보고, 끝으로 e2e testing에 대해서도 간단하게 알아보겠다.

Jest

  • Jest : 자바스크립트 코드를 쉽게 테스팅할 수 있도록 도와주는 프레임워크.
  • Babel, Typescript, Node, React, Angular, Vue 등의 다양한 언어, 프레임워크의 프로젝트 모두 지원한다.

  • nest.js 프로젝트에서는 jest가 미리 설치되어 있고, package.json에 위와 같이 script도 설정되어 있어 따로 설정할 필요는 없다.
  • test로 기본적인 테스트를 실행할 수 있고, test:watch를 통해 지속적 테스팅, test:cov를 통해 테스트 coverage 확인, test:e2e를 통해 e2e 테스팅이 가능하다.

.spec.ts

  • 지금까지 프로젝트를 진행하면서 .spec.ts를 많이 봐왔을텐데, 이것이 테스팅 전용파일이다.
  • 자동 생성 이 외에 새롭게 생성할 수 있는 방법은 없는 것으로 파악된다. (필요할 경우 직접 파일을 만들고 상용구를 복사해 작성해야함)
  • 예를 들어 app.service.spec.ts가 있다고하면 이는 app.service를 테스팅하는 파일인 것이다. 해당 .spec.ts 파일이 존재해야 테스팅이 가능하다.
import { Test, TestingModule } from '@nestjs/testing'; import { MoviesService } from './movies.service'; describe('MoviesService', () => { ​​let service: MoviesService; ​​beforeEach(async () => { ​​​​const module: TestingModule = await Test.createTestingModule({ ​​​​​​providers: [MoviesService], ​​​​}).compile(); ​​​​service = module.get<MoviesService>(MoviesService); ​​}); ​​it('should be defined', () => { ​​​​expect(service).toBeDefined(); ​​}); });
  • 아무것도 하지 않은 .spec.ts 파일은 위와 같은 형태이다. (나는 프로젝트를 진행하다 생성된 movies.service.spec.ts 파일을 사용했다.)
  • describe : 테스트를 묘사한다는 의미로 해당 메소드 내부에 테스팅을 위한 함수를 나열한다. describe안에 describe를 넣어 특정 파일 내 각 함수에 대한 테스트를 구분지어 표현하기도 한다. (테스트 출력 코드를 보면 이해가 빠를 것이다.)
  • beforeEach : 각각에 대한 테스트를 수행하기 전에 실행되는 메소드. 각 테스트에 앞서서 필요한 코드(인스턴스를 생성해놓는 등)를 작성할 수 있다. atfterEach, afterAll, beforeAll도 존재하여 beforeEach와 비슷한 방식으로 사용할 수 있다.
  • it : 실질적으로 테스트를 진행하는 부분. expect를 사용해 함수가 목표하는 바를 정확히 수행하고 있는지 확인하는 코드를 작성한다.

it를 통한 기초 테스트 코드 예시

​​it('shoud be 4', () => { ​​​​expect(2+2).toEqual(4) ​​})
  • describe 내부에 새로운 it를 작성했다.
  • expect를 통해 제공되는 toEqual을 비롯한 수많은 메소드로 쉽게 테스팅 코드를 작성할 수 있다. (toEqual 외에도 toBeInstanceOf, toBeGreaterThan 등 다양한 메소드를 제공한다.)

  • npm run test로 테스트를 진행한 결과 위와 같이 성공 메세지가 출력된다.
  • 하지만 toEqual의 숫자를 4에서 5로 바꾸고 다시 테스트를 진행하면

  • 실패 메세지가 출력됨을 알 수 있다.
  • 이런 방식으로 it를 활용하여 테스트문을 작성하면 된다.

describe의 역할

  • describe는 테스트 결과의 구조를 명확히 확인할 수 있도록 해준다.
import { Test, TestingModule } from '@nestjs/testing'; import { MoviesService } from './movies.service'; describe('MoviesService', () => { ​​let service: MoviesService; ​​beforeEach(async () => { ​​​​const module: TestingModule = await Test.createTestingModule({ ​​​​​​providers: [MoviesService], ​​​​}).compile(); ​​​​service = module.get<MoviesService>(MoviesService); ​​}); ​​it('should be defined', () => { ​​​​expect(service).toBeDefined(); ​​}); ​​describe("getAll()", () => { ​​​​it("should return an array", () => { ​​​​​​const result = service.getAll(); ​​​​​​expect(result).toBeInstanceOf(Array); ​​​​}) ​​}) });
  • 기본적인 .spec.ts 파일에서 describe("MoviesService", ~) 안에 describe("getAll()", ~)을 작성했다.

  • test를 수행한 결과에서 위와 같이 테스팅 구조를 확실하게 확인할 수 있음을 알 수 있다.
  • 하나의 테스팅에 다양한 함수가 포함되어 있을 경우엔 describe로 구조를 확실히 정리했을 때의 효과를 좀 더 명확히 알 수 있다. (아래 결과가 그 예시이다.)


E2E TESTING

  • 지금까지 수행한 것은 unit testing이었다. 끝으로 e2e testing을 간단하게 알아보자.
  • test 폴더 내부에 존재하는 app.e2e-spec.ts를 사용한다.

import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './../src/app.module'; describe('AppController (e2e)', () => { ​​let app: INestApplication; ​​beforeAll(async () => { ​​​​const moduleFixture: TestingModule = await Test.createTestingModule({ ​​​​​​imports: [AppModule], ​​​​}).compile(); ​​​​app = moduleFixture.createNestApplication(); ​​​​app.useGlobalPipes( ​​​​​​new ValidationPipe({ ​​​​​​​​whitelist: true, ​​​​​​​​forbidNonWhitelisted: true, ​​​​​​​​transform: true, ​​​​​​}), ​​​​); ​​​​await app.init(); ​​}); ​​it('/ (GET)', () => { ​​​​return request(app.getHttpServer()) ​​​​​​.get('/') ​​​​​​.expect(200) ​​​​​​.expect('Welcome to my Movie API'); ​​}); ​​describe('/movies', () => { ​​​​it('GET', () => { ​​​​​​return request(app.getHttpServer()) ​​​​​​​​.get('/movies') ​​​​​​​​.expect(200) ​​​​​​​​.expect([]); ​​​​}); ​​​​it('POST', () => { ​​​​​​return request(app.getHttpServer()) ​​​​​​​​.post('/movies') ​​​​​​​​.send({ ​​​​​​​​​​title: 'Test', ​​​​​​​​​​year: 2000, ​​​​​​​​​​genres: ['test'], ​​​​​​​​}) ​​​​​​​​.expect(201); ​​​​}); ​​​​it('DELETE', () => { ​​​​​​return request(app.getHttpServer()) ​​​​​​​​.delete('/movies') ​​​​​​​​.expect(404); ​​​​}); ​​}); ​​describe('/movies/:id', () => { ​​​​it('GET 200', () => { ​​​​​​return request(app.getHttpServer()) ​​​​​​​​.get('/movies/1') ​​​​​​​​.expect(200); ​​​​}); ​​​​it('GET 404', () => { ​​​​​​return request(app.getHttpServer()) ​​​​​​​​.get('/movies/999') ​​​​​​​​.expect(404); ​​​​}); ​​​​it.todo('DELETE'); ​​​​it.todo('PATCH'); ​​}); });
  • 기본적인 구조나 사용방식은 같지만, '요청 자체를' 테스트한다는 관점에서 접근방식이 다르다.
  • request(app.getHttpServer())로 각 요청을 url에 직접보내보고 이에 대한 결과를 expect로 확인하는 방법을 사용했다.
  • 위 코드를 확인해보면 beforeAll에서 테스트를 위한 어플리케이션을 따로 생성함을 확인할 수 있다. 이는 우리가 실제로 사용하는 어플리케이션과는 다른 단지 테스트를 위한 어플리케이션이다. 즉, 테스트를 진행할 때마다 매번 임의의 어플리케이션을 생성해 사용하는 것이다.
  • 주의 : beforeAll 부분을 확인해보면 기본구문 외에 app.useGlobalPipes로 파이프 설정을 해줬음을 알 수 있다. 이는 우리가 실제적으로 돌아가는 환경과 동일한 환경에서 테스트 어플리케이션이 돌아가도록 하기 위함이다. 이를 설정해주지 않을 경우, 생각하지 못한 에러가 발생할 수 있다. 이를 주의하여 항상 같은 환경을 제공할 수 있도록 하자.
  • 결과는 npm run test:e2e로 확인한다.
  • cf. todo는 아직 내용을 작성하지 않았거나 할 수 없지만 테스트를 해야하는 것을 표기할 때 사용한다. 아래 결과를 보면 연필 이모지와 함께 해당 내용이 출력됨을 확인할 수 있다.
  • cf. 보통 구조를 짤 때 unit testing을 하든 e2e testing을 하든 test를 위한 데이터베이스와 실사용을 위한 데이터베이스를 따로 구축한다. 성능 테스트를 진행할 때 데이터의 생성, 삭제, 수정이 빈번하게 일어나기 때문이다.