해당 포스팅은 nest.js 9.0.0 버전, typeorm 0.3.x 버전을 기준으로 작성되었습니다.
모든 글은 작성자의 주관이 100% 담겨있기 때문에 부정확할 수 있습니다.
#pre. 터미널을 켜고 프로젝트 폴더로 이동
위의 링크의 내용을 참고하여 study 폴더로 이동해줍니다.
그리고 code . 명령어를 통해 vscode를 열어줍니다.
이번 포스팅에서는 Jest라는 라이브러리를 활용해서 Unit Test를 진행해보도록 하겠습니다.
Test
테스트란 말그대로 내가 만든 API가 제대로 동작하는지 시험해보는 것입니다.
테스트는 Unit Test(단위 테스트)와 e2e Test(End-to-End, 통합 테스트)가 있습니다.
그리고 테스트를 할 때 보통 Coverage를 100%로 하는 것을 목표로 합니다.
Coverage란
내 코드가 얼마나 테스팅 되고 있는가를 판단하기 위한 척도입니다.
커버리지가 높으면 많은 것들에 대한 검증이 이루어지고 있다는 뜻이니, 커버리지가 높을수록 보통 더 좋습니다.
다만, 커버리지가 100%라고 해서 에러가 없는게 아니라는 점 잊지말아주시기 바랍니다.
제가 테스트에 대해서 자세히 알지는 못하지만, 테스트를 하는 방법은 두 종류로 나눌 수 있습니다.
Mocking으로 Test하는 방법과 Test DB를 따로 두고 Test DB를 주입하는 방법입니다.
Mocking은 실제 서비스에서 사용하는 DB를 테스트에 사용할 수 없어서 똑같은 틀(Mock)을 만들어서 대신 주입해서 테스트하는 방법입니다. Nest.js에서는 Dependency Injection이 가능하기 때문에 사용할 수 있는 방법입니다.
Test DB를 따로 두고 Test DB를 주입하는 방법은 실제 서비스중인 DB대신, 개발자의 Local DB같은 DB를 따로 Test DB로 두고, 실서비스중인 DB대신 Test DB로 테스트를 진행하는 방법입니다.
둘 중에 뭐가 더 좋냐라고 물으신다면 제가 감히 대답해드릴 수는 없습니다. 각각의 장단점이 있기 때문에 취향에 맞는 방법으로 골라서 사용하시면 됩니다.
아래는 제 개인적인 의견이니 혹시 틀린점이 있다면 지적해주시면 감사하겠습니다.
Mocking은 실제 DB를 사용하는 것이 아니기 때문에 DB를 따로 관리할 필요가 없습니다. 다만 Mock을 직접 만들어야하는 단점이 있습니다. 직접 만드는 만큼 제대로 만들지 않으면 안됩니다. 그리고 이러한 이유로 Mocking을 믿지 못하겠다는 사람들도 더러 있습니다. 아무래도 실제 DB를 거치는 테스트가 더 신뢰감이 들겠죠.
Test DB는 실제 DB를 사용하기 때문에 Mocking보다 신뢰성이 있습니다. 다만 Test DB를 관리해야 한다는 점, DB를 건드리는 만큼 트랜잭션 같은 것들을 신경써줘야 한다는 점이 단점입니다.
여기서는 Test DB를 이용해서 작성을 해보겠습니다.
Jest
이제, Jest를 이용한 유닛 테스트를 작성해보도록 하겠습니다.
Typescript는 모듈을 절대경로로 잘 찾지만, Jest는 찾지 못하므로 경로를 설정해 주어야 합니다.
package.json 파일에서 jest 부분을 다음과 같이 수정해줍시다.
"jest": {
...
"moduleNameMapper": {
"src/(.*)": "<rootDir>/$1"
},
...
}
jest에 moduleNameMapper 속성을 위와 같이 추가해 주면 됩니다.
그리고 test폴더를 src폴더 안으로 옮겨준 후, test폴더에 unit-test폴더를 생성해주고, unit-test폴더에 article.spec.ts 파일을 생성해줍시다. 이 파일에서 controller와 service를 둘 다 테스트 하겠습니다.
그리고 .env.test 파일을 만들어서, .env.dev에 있는 내용을 그대로 복사해줍시다. 우리는 로컬 DB를 Test DB로 사용한다고 가정하고 하겠습니다. (어차피 나중에 AWS RDS를 실제 DB로 바꿀 예정입니다)
// .env.test
DB_HOST=localhost
DB_PORT=3306
DB_NAME=study
DB_USER=유저네임(보통 root)
DB_PASSWORD=DB설치시 설정한 비밀번호
JWT_SECRET=secret
그리고 package.json의 scripts를 다음과 같이 변경해줍시다.
"scripts": {
...
"test": "cross-env NODE_ENV=test jest",
...
},
test 앞에 cross-env NODE_ENV=test를 추가해줍시다.
그리고 다음과 같이 코드를 작성해줍시다.
// src/test/unit-test/article.spec.ts
import { ConfigModule, ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ArticleEntity } from 'src/entities/article.entity';
import { CommentEntity } from 'src/entities/comment.entity';
import { UserEntity } from 'src/entities/user.entity';
import { ArticleController } from 'src/res/article/article.controller';
import { ArticleService } from 'src/res/article/article.service';
describe('Article Spec', () => {
let controller: ArticleController;
let service: ArticleService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: `.env.${process.env.NODE_ENV}`,
isGlobal: true,
}),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
retryAttempts: configService.get('NODE_ENV') === 'prod' ? 10 : 1,
type: 'mysql',
host: configService.get('DB_HOST'),
port: Number(configService.get('DB_PORT')),
database: configService.get('DB_NAME'),
username: configService.get('DB_USER'),
password: configService.get('DB_PASSWORD'),
entities: [ArticleEntity, CommentEntity, UserEntity],
synchronize: false,
logging: true,
timezone: 'local',
}),
}),
TypeOrmModule.forFeature([ArticleEntity]),
],
controllers: [ArticleController],
providers: [ArticleService],
}).compile();
controller = module.get<ArticleController>(ArticleController);
service = module.get<ArticleService>(ArticleService);
});
test('0. ArticleTest Setting', async () => {
expect(controller).toBeDefined();
expect(service).toBeDefined();
});
describe('게시글 생성과 삭제', () => {
let article;
test('createArticle Spec ', async () => {
const res = await controller.createArticle(
{
content: 'test content',
title: 'test title',
},
{ id: 1 },
);
console.log(res);
article = res;
expect(res).toBeInstanceOf(Object);
});
test('deleteArticle Spec', async () => {
if (article) {
const res = await controller.deleteArticle(article.id, { id: 1 });
expect(res).toStrictEqual({
affected: 1,
});
}
});
});
});
먼저, describe로 해당 테스트에 대한 설명을 작성할 수 있습니다. (중첩 가능합니다. describe안에 describe 함수 작성 가능)
describe('테스트 명', 함수) 로 작성할 수 있씁니다.
그리고 beforeEach함수를 이용해서 TestingModule을 생성해줍니다. 해당 TestingModule은 ArticleModule과 동일하게 동작을 해야하기 때문에 ArticleModule에 주입된 모듈들과 컨트롤러, 프로바이더(서비스)들이 동일하게 주입되어야 하고, AppModule에서 사용하는 ConfigModule과 TypeOrmModule.forRootAsync도 주입해주어야 합니다. 그 후, compile() 함수를 사용해줍시다.
그리고 controller와 service 변수에 module.get함수를 이용하여 ArticleController와 ArticleService를 할당해줍시다.
이것으로 controller와 service를 ArticleController, ArticleService로 사용할 수 있게 되었습니다.
여기서 주의 사항을 이야기하자면, TypeOrmModule.forRootAsync에서 entities는 jest가 인식하려면 Entity를 직접 넣어주어야 합니다. ArticleEntity만 넣어도 될 것 같지만 ArticleEntity와 연결된(n:n관계로 연결된) Entity들도 넣어주어야 합니다.
그리고 test(혹은 it)함수를 통해 테스트 단위를 설정할 수 있습니다.
처음에는 controller와 service가 잘 주입이 되었는지 확인하는 함수를 작성하였습니다.
test('0. ArticleTest Setting', async () => {
expect(controller).toBeDefined();
expect(service).toBeDefined();
});
expect(controller).toBeDefined() : controller라는 변수가 정의되있을 것(toBeDefined)이라고 예상한다. 즉, controller가 undefined가 아니면 테스트를 통과한다.
그리고 간단하게 게시글 작성과 삭제에 대한 테스트를 작성해보았습니다.
describe('게시글 생성과 삭제', () => {
let article;
test('createArticle Spec ', async () => {
const res = await controller.createArticle(
{
content: 'test content',
title: 'test title',
},
{ id: 1 },
);
console.log(res);
article = res;
expect(res).toBeInstanceOf(Object);
});
test('deleteArticle Spec', async () => {
if (article) {
const res = await controller.deleteArticle(article.id, { id: 1 });
expect(res).toStrictEqual({
affected: 1,
});
}
});
});
test('테스트 명', 함수) 로 테스트를 작성할 수 있습니다.
createArticle Spec테스트를 봐봅시다.
const res = await controller.createArticle() 를 이용해서 DB 게시글을 생성하고 결과값을 res에 담습니다.
그리고 expect(res).toBeInstanceOf(Object) 를 통해 객체가 넘어오는 것을 확인하면 테스트 통과입니다.
원래는 이런식으로 하지 않고, 타입추론이 된다는 가정 하에 toStrictEqual을 사용합니다.
expect(res).toStrictEqual({
id: '1',
title: 'test title',
content: 'test content',
userId: '1',
...
})
여기서는 Auto Increment 때문에 id값이 계속 바뀌기 때문에 객체가 넘어오는지만 확인하였습니다.
그리고 deleteArticle Spec 테스트를 봐봅시다.
만약 article이 존재한다면, 즉 article이 생성이 되었다면 deleteArticle함수를 실행시킵니다.
그리고 expect(res).toStrictEqual을 통해서 affected가 1인지 확인하면 완료입니다.
이런식으로 테스트를 작성하면 됩니다. 제가 조금... 대충 작성한 감은 있지만, 이런식으로 테스트를 작성하는구나~ 라고 감만 잡아주시면 될 것 같습니다.
그러면, Test를 실행해봅시다.
터미널을 켜고, npm run test 명령을 입력해줍시다. (항상 말하지만, 명령어는 최상위 폴더에서 입력해야합니다)
그러면 터미널에 다음과 같은 결과가 출력됩니다.
만약, 에러가 뜬다면 테스트가 실패한 것입니다.
여기서 주의할 점은 테스트를 완벽하게 작성한다는 가정하에 개발이 이루어져야 한다는 점입니다. 테스트가 실패한다고 테스트 코드를 고치는건 말이 되지 않습니다. 테스트 코드를 작성하는 이유는 실제 코드가 제대로 동작하는지 여부를 확인하기 위한 것인데, 테스트가 실패한다고 테스트 코드를 고치지는 맙시다. 그리고 테스트 코드를 완벽하게(꼼꼼하게) 작성해놓읍시다.
Github에 Push
자, 여기까지 했으면 Github에 Push해봅시다.
참고하시라고 올려두는 사진입니다.
마지막에 git push가 아니라 git push origin main으로 해줍시다.
'Back-end > 기초부터 따라하는 nest.js' 카테고리의 다른 글
#18. 기초부터 따라하는 Nest.js : AWS EC2에 서비스 배포하기 (완) (53) | 2023.05.29 |
---|---|
#17. 기초부터 따라하는 Nest.js : AWS RDS로 데이터베이스 연동하기 (0) | 2023.05.29 |
#15. 기초부터 따라하는 Nest.js : Class-validator와 Pipe (0) | 2023.05.20 |
#14. 기초부터 따라하는 Nest.js : Exception Filter (0) | 2023.05.17 |
#13. 기초부터 따라하는 Nest.js : Interceptor (0) | 2023.05.17 |