[회고] 신입 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를 위한 데이터베이스와 실사용을 위한 데이터베이스를 따로 구축한다. 성능 테스트를 진행할 때 데이터의 생성, 삭제, 수정이 빈번하게 일어나기 때문이다.

반응형