Nest.js

[Nest.js] jest를 이용한 테스트 코드 작성

Hoo_Dev 2023. 1. 13. 15:31

 

정보 전달의 포스팅 보단 학습 메모 목적의 포스팅 입니다. 😢
부족한 설명과 내용에 대해 미리 양해 부탁드립니다.

  • npm run test:cov - 코드가 얼마나 테스팅 됐는지 퍼센티지로 알려줌.
  • npm run test:watch - 저장 할 때 마다 테스트를 진행.

유닛 테스트

  • 모든 함수를 따로 테스트
  • 서비스에서 분리된 유닛을 테스트
  • spec.ts 파일들을 찾아서 테스트 해준다.

E2E 테스트

  • 모든 시스템을 테스팅
  • 사용자의 관점에서의 테스팅

우선 유닛 테스트를 진행해보자

유닛테스트

import { NotFoundException } from '@nestjs/common';
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('배열 반환 해줘', () => {
      // 1. getAll 호출
      const result = service.getAll();
      // 2. result가 Array인지 테스트
      expect(result).toBeInstanceOf(Array);
    });
  });

  describe('getOne()', () => {
    it('영화 반환 해줘', () => {
      // 한 개의 영화 목록에 대한 테스트를 위해 데이터를 생성
      service.create({
        title: 'testMovie',
        year: 2022,
        genres: ['액션', '코미디'],
      });
      const movie = service.getOne(1);
      // 영화 객체가 반환 되는지 여부
      expect(movie).toBeDefined();
      // 영화의 아이디가 1인지의 여부
      expect(movie.id).toEqual(1);
    });
    it('해당 영화가 없으면 404에러 던져줘', () => {
      try {
        service.getOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
        expect(e.message).toEqual('Movie with ID: 999');
      }
    });

    describe('deleteOne()', () => {
      it('영화 지워줘', () => {
        service.create({
          title: 'testMovie',
          year: 2022,
          genres: ['액션', '코미디'],
        });
        const beforeDelete = service.getAll();
        service.deleteOne(1);
        const afterDelete = service.getAll();

        expect(afterDelete.length).toEqual(beforeDelete.length - 1);
      });

      it('삭제 할 영화 없으면 404에러 던져줘', () => {
        try {
          service.deleteOne(9999);
        } catch (e) {
          expect(e).toBeInstanceOf(NotFoundException);
          expect(e.message).toEqual('Movie with ID: 9999');
        }
      });
    });

    describe('create()', () => {
      it('영화 만들어줘', () => {
        const beforeCreate = service.getAll().length;

        service.create({
          title: 'testMovie',
          year: 2022,
          genres: ['액션', '코미디'],
        });

        const afterCreate = service.getAll().length;
        expect(afterCreate).toBeGreaterThan(beforeCreate);
      });
    });

    describe('update()', () => {
      it('영화 수정 해줘', () => {
        service.create({
          title: 'testMovie',
          year: 2022,
          genres: ['액션', '코미디'],
        });

        service.update(1, { title: 'updateTestMovie' });
        const movie = service.getOne(1);
        expect(movie.title).toEqual('updateTestMovie');
      });
    });
  });
});

404 에러 메세지 테스팅에서 생긴 일

  • 404에러를 테스팅 하던 중 만난 에러
  • 999뒤에 공백이 하나 있었다. (코드 작성하다가 실수로 넣은 공백)
  • 중요한 테스트는 아니었지만, 예상치 못하게 테스트 코드 작성의 순 기능을 알게 됐다. (사소한 실수로 인한 에러(오타같은 것)를 미리 잡아 낼 수 있었다.)

모든 유닛 테스트를 마친 후 커버리지

E2E 테스트

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;

  // beforeEach 테스트 마다 연결을 해준다. 즉 테스트 함수들이 실행 될 때 마다 실행 된다.
  // beforeAll() -> 테스트 함수마다 매번 연결을 맺고 끊는 것이 아닌 맨 처음에 한번 연결을 하고
    // 여러 함수 테스트에 걸쳐서 사용한 후 마지막에 연결을 끊는다.
  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    // main.ts 에서 사용한 transform이 테스트 서버에선 작동 하지 않는다. 
        // 즉 id값이 string으로 동작하게 되므로 getOne() 함수의 테스트가 이뤄지지 않는다.
    // 그렇기 때문에 테스트 app 안에서도 ValidationPipe를 작성해주면서 
        // 실제 앱과 같은 환경으로 설정해준다.
    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 201', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: 'Test',
          year: 2000,
          genres: ['test'],
        })
        .expect(201);
    });
    it('POST 400', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: 'Test',
          year: 2000,
          genres: ['test'],
          other: 'thing',
        })
        .expect(400);
    });
    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('PATCH 200', () => {
      return request(app.getHttpServer())
        .patch('/movies/1')
        .send({ title: 'updateTitle' })
        .expect(200);
    });
    it('DELETE', () => {
      return request(app.getHttpServer()).delete('/movies/1').expect(200);
    });
  });
});