9시 24분
NestJS - Unit testing과 E2E testing 본문
테스팅에 대한 소개
package.json 파일을 보면 테스팅과 관련된 스크립트가 5개 정도 있다.
jest는, 자바스크립트를 쉽게 테스팅하는 npm 패키지로, .spec.ts로 된 테스트를 포함한 파일들을 테스트한다.
test:cov 코드가 얼마나 테스팅 됐는지 또는 안됐는지를 보여준다.
test:watch 모든 테스트 파일을 찾아 무슨 일이 있는지 관찰한다.
테스트의 종류에는 E2E(End-To-End) 테스팅, Integration 테스팅, Unit 테스팅이 있는데 강의에서는 E2E(End-To-End) 테스팅과 Unit 테스팅을 다룬다.
Unit 테스팅 - 함수나 서비스에서 분리된 유닛 같이 테스트할 수 있는 가장 작은 단위를 독립적으로 테스트 방법이다.
ex) getAll() 함수만 테스팅하고 싶을 때 사용
E2E(End-To-End) 테스팅 - 모든 시스템을 테스팅하며, 내가 만든 페이지로 들어가서 의도한 대로 기능이 동작하는지 테스트한다. 사용자가 취할만한 액션을 모두 테스트한다.
ex) 사용자 관점에서 특정 링크 클릭시 그 링크 가는지
Unit testing
movies.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();
});
});
- describe - 테스트를 묘사하는 부분
- beforeEach - 테스트를 하기 전 실행되는 부분
- 15~18줄이 테스트를 실제 하고 있는 부분이다.
이번엔 우리가 직접 테스트를 작성해보자.
it('first test i write', ()=>{
expect(2+2).toEqual(4);
});
첫 번째 인자로 들어가는 문자열 값은, 자신이 원하는대로 설정하면 되고, 두 번째 인자인 함수에서 테스트릴 하면 된다.
watch 모드로 테스트를 하고 있기 때문에, 파일에 변경 사항이 생기면 그 결과를 바로 볼 수 있다.
코드를 의도적으로 틀리도록 수정해보자.
it('first test i write', ()=>{
expect(2+2).toEqual(5);
});
유닛 테스트는 오류를 뱉으며 어떤 조건을 받았으며, 어떤 결과가 나와야하는지 자세하게 알려준다.
이번에는 우리가 전에 작성한 REST API (getAll, getOne, create, update, delete)를 테스팅해보자.
먼저 getAll()을 해보자.
getAll()이 배열을 리턴하는지 안하는지(배열 인스턴스인지) 확인해볼 것이다.
describe('getAll', () => {
it('should return an array', () => {
const result = service.getAll();
expect(result).toBeInstanceOf(Array);
});
});
위에서 MoviesService가 service라는 변수로 선언되어서 함수에 쉽게 접근할 수 있다.
굿~
이번엔 getOne()을 해보자.
getOne()을 테스트할 때 Movie가 생성되어 있지 않다면 문제가 될 수 있으니, movie 객체를 만든 뒤 테스트하자.
getOne()에서는 movie 객체를 리턴하는지와, id가 1인지, 404에러를 제대로 throw하는지를 테스트한다.
describe('getOne', () => {
it('should return a movie & id is 1', () => {
service.create({
"title": "Test Movie",
"director": "me"
});
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);
}
});
});
굿 ~
그리고 지금 npm run test:cov를 해보면, movies.service.ts에 대한 테스트 비율이 높아진 것을 확인할 수 있다.
우리가 movies.service.ts의 63.16%를 테스팅하고 있다는 뜻이다.
이번엔 deleteOne()을 해보자.
영화를 삭제하는 기능과, 올바르지 않은 input이 들어왔을 때 404 에러를 던지는지 테스트한다.
영화를 삭제하는 기능을 테스트할 때, 삭제하기 전 배열의 길이와 삭제 후 배열의 길이를 비교하는 방법으로 테스트를 진행하였다.
describe('deleteOne', () => {
it('should delete a movie', () => {
service.create({
"title": "Test Movie",
"director": "me"
});
const allMovies = service.getAll().length;
service.deleteOne(1);
const afterDelete = service.getAll().length;
expect(afterDelete).toBeLessThan(allMovies);
});
it('should throw 404 error', () => {
try{
service.deleteOne(999);
} catch(e){
expect(e).toBeInstanceOf(NotFoundException);
}
});
});
굿 ~
이번엔 create()을 해보자.
영화 생성 전 배열의 길이와 생성 후 배열의 길이를 비교하는 방법으로 테스트를 했다.
describe('create', () => {
it('should create a movie', () => {
const beforeCreate = service.getAll().length;
service.create({
"title": "Test Movie",
"director": "me"
});
const afterCreate = service.getAll().length;
expect(afterCreate).toBeGreaterThan(beforeCreate);
});
});
굿 ~
마지막으로, update()를 해보자.
이제까지 테스트마다 movie를 생성했는데, beforeEach에 넣는 방법도 있다. 일단은 기존에 하던대로 movie를 생성하자.
영화 정보를 임의로 수정한 뒤 수정한 내용과 일치하는지 확인하는 방법으로 테스트를 하였고, 올바르지 않은 input에 대해 404 에러를 던지는지 테스트하였다.
describe('update', () => {
it('should update a movie', () => {
service.create({
"title": "Test Movie",
"director": "me"
});
service.update(1, {
"title": "Updated test"
});
const movie = service.getOne(1);
expect(movie.title).toEqual('Updated test');
});
it('should throw 404 error', () => {
try{
service.update(999, {});
} catch(e){
expect(e).toBeInstanceOf(NotFoundException);
}
});
});
굿 ~
이로써 movies.service.ts에 있는 모든 함수들을 테스팅해보았고, 그 결과 coverage가 100%가 된 것을 확인할 수 있다 ~~
** 지금 한 테스트 외에도 여러 가지 떠오르는 기능들을 테스트해볼 수 있음. 여기서 한 내용에 대한 테스트만 할 수 있는 것 아님
+ afterAll, beforeAll이라는 것도 있다.
ex) afterAll - 테스트를 진행한 DB를 깔끔하게 지우는 function을 넣는다.
E2E 테스팅
E2E 테스팅은 test 폴더가 필요한데, 이는 Nest cli 덕분에 이미 생성되어 있다.
유닛 테스팅하기 어려울 때 어떨 땐 하나만, 어떨 땐 2개 이상
e2e 무비와 관련된 애플리케이션의 모든 부분을 테스트
[ app.e2e-spec.ts ]
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
});
이것이 초기 app.e2e-spec.ts 코드이다.
beforeEach, expect, describe처럼 유닛 테스트할 때 봤던 익숙한 코드들이 있어
다른 점 supertest라는 라이브러리 패키지
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!');
});
이 부분이 request 같은 거, GET 방식으로 API request를 보내
'/' url을 받고, 200 코드와 'Hello World!'를 expect한다.
라는 뜻
처음에 에러 나서 강의 코드랑 비교해보니 나는 app.controller.ts가 없음 !
그래서 app.controller.ts 생성해줌
import { Controller, Get } from "@nestjs/common";
@Controller('')
export class AppController{
@Get()
home(){
return 'Welcome to my Movie API';
}
}
+ app.module.ts에 controller 추가
그 다음 npm run test:e2e하니,
굿 ~
어떤 것에 대해 E2E 테스팅을 해볼 수 있을까?
/movies, /:id, create, delete, patch를 테스트할 수 있다.
/movies
it('/movies (GET)', () => {
return request(app.getHttpServer())
.get('/movies')
.expect(200)
.expect([]);
});
굿 ~
create
그 전에, describe 이용해서 구조를 깔끔하게 했다.
우선 데이터를 하나 보내고, create가 잘 되는지 테스트했다.
describe('/movies', () => {
it('GET', () => {
return request(app.getHttpServer())
.get('/movies')
.expect(200)
.expect([]);
});
it('POST', () => {
return request(app.getHttpServer())
.post('/movies')
.send({
"title": "Test movie",
"director": "me"
})
.expect(201);
});
});
굿 ~
delete
it('DELETE', () => {
return request(app.getHttpServer())
.delete('/movies')
.expect(404);
});
굿 ~
이로써 /movies end point에 대한 E2E 테스팅 완료했다.
( + DELETE은 /movies end point에 속하지 않지만, 404 에러가 났을 때의 상황을 보여주기 위해 추가되었다. )
다음으로 /:id에 대한 E2E 테스팅을 해보자.
it.todo
아래 사진과 같이 연필 모양으로 테스트를 해야할 목록들이 결과에 표시된다.
describe('/movies/:id', () => {
it.todo("GET");
it.todo("DELETE");
it.todo("PATCH");
});
Nest는 매 테스트마다 어플리케이션 실행하는데, 여기서의 어플리케이션은 브라우저에서 실행하는 진짜 어플리케이션이랑 다르다.
테스팅을 시작하기 전에 새 어플리케이션을 만들고 싶고, POST에서 데이터 send하는 그런 과정을 생략하고 싶다.
그러기 위해 beforeEach를 beforeAll로 바꾸자.
그러면 앞서 POST에서 생성된 데이터베이스가 기억되게 된다.
하지만, 에러가 발생하였다 !!
실제 서버에서는 정상적으로 작동하지만, 테스트 서버에서는 작동이 되지 않는다.
이것은 실제 서버와 테스트 서버에서의 id의 타입이 다르기 때문이다.
실제 서버 id number, 테스트 서버에서 id는 string이다.
그 이유는 transform이 테스트 서버에서는 적용되지 않았기 때문이다.
아래와 같이 beforeAll 안에서 앱을 생성하는 부분 아래 파이프를 만들어주자. ( pipe import도 해주기 )
app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true
}));
테스팅 환경에 실제 구동 환경의 설정을 그대로 적용시켜주자 !!
이제 moive id를 GET하는 것을 테스트해보자.
describe('/movies/:id', () => {
it('GET 200', () => {
return request(app.getHttpServer())
.get('/movies/1')
.expect(200);
});
});
굿 ~
이번에는 PATCH를 테스트해보자.
it('PATCH', () => {
return request(app.getHttpServer())
.patch('/movies/1')
.send({ "title": "Updated test" })
.expect(200);
});
굿 ~
다음으로 DELETE를 테스트해보자.
it('DELETE', () => {
return request(app.getHttpServer())
.delete('/movies/1')
.expect(200);
});
굿 ~
마지막으로, 잘못된 데이터를 POST해보자.
it('POST 400', () => {
return request(app.getHttpServer())
.post('/movies')
.send({
"title": "Test movie",
"director": "me",
"wrong": "data"
})
.expect(400);
});
굿 ~
이로써 유닛 테스팅과 E2E 테스팅을 모두 해보았다 !
NestJS로 API 만들기 완강 ~ !! ( •̀ ω •́ )y
참고
'Javascript > Node.js' 카테고리의 다른 글
NestJS - Movie REST API 만들기 (0) | 2021.08.24 |
---|---|
NestJS를 들어가며 (0) | 2021.08.23 |
HTML/CSS로 프론트 작업을 할 때 주의사항 (0) | 2021.08.04 |
API와 REST API (0) | 2021.07.27 |
간단한 API 만들기 (0) | 2021.07.22 |