NestJS로 API 만들기

https://nomadcoders.co/nestjs-fundamentals


노마드코더 강의를 참고하고 있습니다.

 


e2e-spec.ts

Nest.js에서 E2E(End to End) 테스트는 애플리케이션의 전체적인 동작을 테스트하는 방법 중 하나이다.

여러 개의 e2e-spec.ts 파일이 있을 수 있고, E2E 테스트는 사용자가 실제 애플리케이션을 사용할 때와 유사한 환경에서 동작을 검증할 수 있다.

실제 HTTP 요청을 서버에 보내고, 응답을 검증하기 때문에 테스트할 때에도 실제 애플리케이션과 동일한 환경을 세팅해야 한다.

 

1. app.e2e-spec.ts

GET 요청 테스트

describe('/movies', ~), describe('/movies/:id', ~) 2가지 엔드포인트에 대해 GET 요청을 테스트한다.

GET 요청 시, 값이 있으면 200 응답이 예측되고, 값이 없으면 404 응답이 예측된다.

describe('AppController (e2e)', () => {
  describe('/movies', () => {
    it('GET 200', () => {
      return request(app.getHttpServer())
        .get('/movies')
        .expect(200)
        .expect([])
    });
  });

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

 

 

POST 요청 테스트

describe('/movies', ~), describe('/movies/:id', ~) 2가지 엔드포인트에 대해 POST 요청을 테스트한다.

POST 요청 시, 올바른 값을 전송하면 201 응답이 예측되고, 잘못된 값을 전송하면 400 응답이 예측된다.

describe('AppController (e2e)', () => {
  describe('/movies', () => {
    it('POST 201', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: 'Test',
          year: 2024,
          genres: ['Test'],
        })
        .expect(201)
    });

    it('POST 400', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          name: 'Name',
        })
        .expect(400)
    });
  });
});

 

DELETE 요청 테스트

describe('/movies', ~), describe('/movies/:id', ~) 2가지 엔드포인트에 대해 DELETE 요청을 테스트한다.

DELETE 요청 시, 전체 배열은 삭제할 수 없기 때문에 404 응답이 예측되고, 하나의 배열은 삭제할 수 있기 때문에 200 응답이 예측된다.

describe('AppController (e2e)', () => {
  describe('/movies', () => {
    it('DELETE 404', () => {
      return request(app.getHttpServer())
        .delete('/movies')
        .expect(404)
    });
  });

  describe('/movies/:id', () => {
    it('DELETE 200', () => {
      return request(app.getHttpServer())
        .delete('/movies/1')
        .expect(200)
    });
  });
});

 

PATCH 요청 테스트

describe('/movies/:id', ~) 엔드포인트에 대해 PATCH 요청을 테스트한다.

PATCH 요청 시, 올바른 값을 전송하면 200 응답이 예측되고, 잘못된 값을 전송하면 400 응답이 예측된다.

describe('AppController (e2e)', () => {
  describe('/movies/:id', () => {
    it('PATCH 200', () => {
      return request(app.getHttpServer())
        .patch('/movies/1')
        .send({ title: 'Update title' })
        .expect(200)
    });
    
    it('PATCH 400', () => {
      return request(app.getHttpServer())
        .patch('/movies/1')
        .send({ name: 'Update name' })
        .expect(400)
    });
  });
});

 

2. 테스트 환경 맞추기

  • test/app.e2e-spec.ts

src/main.ts 파일에서 파이프 옵션을 설정했다.

E2E 테스트는 실제 애플리케이션과 테스트 환경이 동일해야 하기 때문에 main.ts에서 설정한 옵션들을 spec.ts 파일에도 동일하게 설정한다. (app.useGlobalPipes() 부분)

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

  describe('/movies', () => {
      ...
  });
});

 

3. 터미널에서 확인하기

터미널에서 아래 명령어를 실행한다.

e2e-spec 파일에 대해 테스트가 진행된다.

npm run test:e2e

NestJS로 API 만들기

https://nomadcoders.co/nestjs-fundamentals


노마드코더 강의를 참고하고 있습니다.

 


spec.ts

Nest.js에서 spec.ts 파일은 유닛 테스트를 작성하는 데 사용하는 파일이다.

특정 모듈, 서비스, 컨트롤러 등의 단위에 대해 테스트를 정의하는 데 사용한다.

 

일반적으로 Nest.js에서 유닛 테스트를 하기 위해 Jest 프레임워크를 사용한다.

package.json 파일을 보면 이미 jest 설정이 되어있는 것을 확인할 수 있다.

 

Jest

  • describe 함수

describe 함수는 테스트 파일이나 테스트 블록을 정의할 때 사용한다.

describe('Math operations', () => {
  // 덧셈에 대한 테스트 그룹
  describe('Addition', () => {
    it('should correctly add two numbers', () => {
      const result = 1 + 2;
      expect(result).toBe(3);
    });
  });

  // 뺄셈에 대한 테스트 그룹
  describe('Subtraction', () => {
    it('should correctly subtract two numbers', () => {
      const result = 5 - 2;
      expect(result).toBe(3);
    });
  });
});

 

  • it 함수

it 함수는 테스트 케이스를 정의할 때 사용한다.

it('테스트 설명', () => {
  // 테스트 로직
});

 

  • expect 함수

expect 함수는 예상한 결과와 실제 결과를 비교하고, 테스트가 성공적으로 통과했는지 여부를 판단한다.

밑에 예시에서 result가 배열의 인스턴스인지 검사한다.

it('should return an array', () => {
  const result = someFunction();
  expect(result).toBeInstanceOf(Array);
});

 


1. service.spec.ts

getAll 메서드 테스트

  1. getAll 메서드로 가지고 온 값이 Array 인스턴스인지 확인한다.
describe('MoviesService', () => {
  describe('getAll', () => {
    it('should return an array', () => {
      const result = service.getAll();
      expect(result).toBeInstanceOf(Array);
    });
  });
});

 

getOne 메서드 테스트

  1. service.create로 영화 데이터를 만든다.
  2. getOne 메서드로 id가 1인 영화 데이터를 가지고 와 id가 1이 맞는지 비교한다.
  3. id가 1이 아니라면(getOne(999)) 404 에러를 발생시킨다.
describe('MoviesService', () => {
  describe('getOne', () => {
    it('should return a movie', () => {
      service.create({
        title: 'Test Movie',
        year: 2024,
        genres: ['Test'],
      });

      const movie = service.getOne(1);
      expect(movie).toBeDefined();
      expect(movie.id).toEqual(1);
    });

    it('should throw 404 error', () => {
      try {
        service.getOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

 

deleteOne 메서드 테스트

  1. service.create로 영화 데이터를 만든다.
  2. getAll 메서드로 영화의 길이를 변수에 저장한다.
  3. deleteOne 메서드로 id가 1인 영화를 삭제한다.
  4. 그 후 다시 getAll 메서드로 영화의 길이를 변수에 저장해 영화를 삭제하기 전과 후의 값을 비교한다.
  5. 없는 id의 영화(deleteOne(999))를 삭제하면 404 에러를 발생시킨다.
describe('MoviesService', () => {
  describe('deleteOne', () => {
    it('deletes a movie', () => {
      service.create({
        title: 'Test Movie',
        year: 2024,
        genres: ['Test'],
      });

      const beforeDelete = service.getAll().length;
      service.deleteOne(1);

      const afterDelete = service.getAll().length;
      expect(afterDelete).toBeLessThan(beforeDelete);
    });

    it('should return a 404', () => {
      try {
        service.deleteOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

 

create 메서드 테스트

  1. getAll 메서드로 영화의 길이를 변수에 저장한다.
  2. create 메서드로 새로운 영화를 생성한다.
  3. 그 후 다시 getAll 메서드로 영화의 길이를 변수에 저장해 영화를 생성하기 전과 후의 값을 비교한다.
describe('MoviesService', () => {
  describe('create', () => {
    it('should create a movie', () => {
      const beforeCreate = service.getAll().length;

      service.create({
        title: 'Test Movie',
        year: 2024,
        genres: ['Test'],
      });

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

 

update 메서드 테스트

  1. service.create로 영화 데이터를 만든다.
  2. update 메서드로 id가 1인 영화의 title을 수정한다.
  3. getOne 메서드로 해당 영화를 가져와 영화의 title이 'Updated Test'가 맞는지 비교한다.
  4. 없는 id의 영화(update())를 수정하면 404 에러를 발생시킨다.
describe('MoviesService', () => {
  describe('update', () => {
    it('should throw a NotFoundException', () => {
      service.create({
        title: 'Test Movie',
        year: 2024,
        genres: ['Test'],
      });

      service.update(1, { title: 'Updated Test' });

      const movie = service.getOne(1);
      expect(movie.title).toEqual('Updated Test');

      try {
        service.update(999, {});
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

 

2. 터미널에서 확인하기

터미널에서 아래 명령어를 실행한다.

모든 spec 파일에 대해 테스트가 진행된다.

 

:watch 옵션을 사용하면 저장할 때마다 자동으로 테스트를 시작한다.

npm run test
또는
npm run test:watch

 

3. 전체 코드

  • src/movies/movies.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';
import { NotFoundException } from '@nestjs/common';

describe('MoviesService', () => {
  let service: MoviesService;
  let testMovie;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [MoviesService],
    }).compile();

    service = module.get<MoviesService>(MoviesService);

    // 테스트에 사용될 영화 객체 생성, id는 자동으로 생성됨
    testMovie = {
      title: 'Test Movie',
      year: 2024,
      genres: ['Test'],
    };
  });

  describe('getAll', () => {
    it('should return an array', () => {
      expect(service.getAll()).toBeInstanceOf(Array);
    });
  });

  describe('getOne', () => {
    it('should return a movie', () => {
      service.create(testMovie);

      const movie = service.getOne(1);
      expect(movie).toBeDefined();
      expect(movie.id).toEqual(1);
    });

    it('should throw 404 error', () => {
      try {
        service.getOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });

  describe('deleteOne', () => {
    it('deletes a movie', () => {
      service.create(testMovie);

      const beforeDelete = service.getAll().length;
      
      service.deleteOne(1);

      const afterDelete = service.getAll().length;
      expect(beforeDelete).toBeLessThan(afterDelete);
    });

    it('should return a 404', () => {
      try {
        service.deleteOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });

  describe('create', () => {
    it('should create a movie', () => {
      const beforeCreate = service.getAll().length;

      service.create(testMovie);

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

  describe('update', () => {
    it('should throw a NotFoundException', () => {
      service.create(testMovie);

      service.update(1, { title: 'Updated Test' });

      const movie = service.getOne(1);
      expect(movie.title).toEqual('Updated Test');

      try {
        service.update(999, {});
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
      }
    });
  });
});

+ Recent posts