Node.js와 Jest로 테스트 코드를 작성하는 기본적인 TDD 강의는 있지만 실제 동작 코드를 테스트하는 것은 어려운데 반해
보고 따라하며 감을 잡을만한 가이드가 없었기 때문에 직접 작성하기로 했다.
(배우면서 작성한 것이라 틀린 내용이 있을 수 있습니다)
Typescript // Node.js v18 // Jest v29
tsconfig.json
{
"ts-node": {
"files": true
},
"exclude": ["node_modules", "build"],
"compilerOptions": {
"strict": true,
"lib": ["es6"],
"moduleResolution": "node",
"target": "ES6",
"allowSyntheticDefaultImports": true,
"typeRoots": ["node_modules/@types"],
"types": ["node", "jest"],
"module": "commonjs",
"resolveJsonModule": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true
}
}
package.json
"jest": {
"transform": {
"^.+\\.ts$": "ts-jest"
},
"testRegex": "\\.test\\.ts$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"json"
],
"globals": {
"ts-jest": {
"diagnostics": true
}
}
},
"scripts": {
"test": "jest --runInBand --detectOpenHandles --forceExit",
"start": "tsc && node server"
},
테스트할 코드
/* registration */
const registration: RequestHandler = async (req, res, next) => {
const { email, nick, password } = req.body;
try {
const exUser = await User.findOne({ where: { email } });
if (exUser) {
return res.status(409).json({ Error: "Account already exists" });
}
const hashPassword = await bcrypt.hash(password, 12);
await User.create({
email,
nick,
password: hashPassword,
});
return res.status(201).end();
// 생성 성공 Status 201
} catch (error) {
console.error(error);
return next(error);
}
};
테스트
import { Request, Response, NextFunction } from "express";
import { registration } from "../../controller/auth.js";
import { login } from "../../controller/auth.js";
import httpMocks from "node-mocks-http";
jest.mock("../../models/user.js");
import User from "../../models/user.js";
// jest.mock 아래 써야 모킹이 된다
jest.mock("bcrypt");
import bcrypt from "bcrypt";
let req: Request, res: any, next: NextFunction;
// beforeEach 위에서 선언 해주어야 각각 넣어줄 수 있다.
beforeEach(() => {
req = httpMocks.createRequest();
res = httpMocks.createResponse();
next = jest.fn();
});
describe("createUser", () => {
beforeEach(() => {
// 각각의 요소에 이메일 등록에 사용될 req.body를 넣어준다.
req.body = {
email: "temp@naver.com",
nick: "temp",
password: "temp_password",
};
// Registration 내부에서 사용할 함수도 모킹해준다.
// 테스트 통과를 가정하고 특정 value를 return하도록 mockResolvedValue
(User.findOne as jest.Mock).mockResolvedValue(null);
(bcrypt.hash as jest.Mock).mockResolvedValue("hashed_password");
(User.create as jest.Mock).mockResolvedValue({});
});
it("이메일이 없을때 유저 생성", async () => {
await registration(req, res, next);
// registration 비동기적으로 시행.
expect(res.statusCode).toEqual(201);
// res의 statusCode가 201로 return된다.
});
it("이미 등록된 이메일이 있다면 error 출력", async () => {
(User.findOne as jest.Mock).mockResolvedValue({
email: "user",
// User.findOne이 값을 찾은 경우 (등록된 이메일이 있는 경우)
});
await registration(req, res, next);
// registration 비동기적으로 시행.
expect(res.statusCode).toEqual(409);
// statusCode가 200이다.
expect(res._getJSONData()).toStrictEqual({
Error: "Account already exists",
});
// json으로 오는 data가 Error: "Account already exists"이다.
});
it("유저 생성하면서 에러 발생", async () => {
const errorMessage = { message: "Error" };
// reject 메시지
(User.findOne as jest.Mock).mockResolvedValue(Promise.reject(errorMessage));
// 비동기 Promise.reject로
await registration(req, res, next);
expect(next).toHaveBeenCalledWith(errorMessage);
});
});
설명
1. jest.mock을 통해 register에 사용되는 요소들을 모킹한다.
jest.mock("../../models/user.js");
import User from "../../models/user.js";
// jest.mock 아래 써야 모킹이 된다
jest.mock("bcrypt");
import bcrypt from "bcrypt";
jest.fn() = 하나하나씩 모킹 처리 해줄때 사용한다.
jest.mock() = 그룹을 한번에 모킹 처리한다(내부의 메서드를 그대로 사용할 수 있다).
이렇게 모킹을 할때는 import 구문보다 mock이 위에 있어야 한다.
2. node-mocks-http를 사용해 req, res, next를 만들어준다.
let req: Request, res: any, next: NextFunction;
// beforeEach 위에서 선언 해주어야 각각 넣어줄 수 있다.
beforeEach(() => {
req = httpMocks.createRequest();
res = httpMocks.createResponse();
next = jest.fn();
});
직접 req, res, next를 만들어줄 수도 있지만 내부 함수 등이 지정되지 않아 직접 만들어주어야 한다.
또한 status: jest.fn(() => res)처럼 직접 만들어 주었더니 테스트 상황에서 status 관련 오류가 발생했기 때문에
req, res, next를 대신 지정해 주는 node-mocks-http 모듈을 사용한다.
req: Request, res: any, next: NextFunction;
위와 같이 타입 지정을 하였다. Response에는 res._getJSONData()가 타입 지정이 되어 있지 않아 사용할 수 없어 일단 any로 설정 해두었다.
res._getJSONData()는 node-mocks-http 공식문서에도 있는 함수이니 그대로 사용해도 될것같다.
3. req.body를 넣어준다.
req.body = {
email: "temp@naver.com",
nick: "temp",
password: "temp_password",
};
다음과 같이 req.body를 넣어준다.
4. Register의 내부에서 사용하는 함수들을 모킹해준다.
(User.findOne as jest.Mock).mockResolvedValue(null);
(bcrypt.hash as jest.Mock).mockResolvedValue("hashed_password");
(User.create as jest.Mock).mockResolvedValue({});
타입 에러는 as jest.Mock을 써서 해결한다.
테스트 통과를 가정하고 특정 value를 return하도록 mockResolvedValue를 지정해준다.
5. 테스트 케이스1 작성
it("이메일이 없을때 유저 생성", async () => {
await registration(req, res, next);
// registration 비동기적으로 시행.
expect(res.statusCode).toEqual(201);
// res의 statusCode가 201로 return된다.
}
이메일이 없는 경우 res.statusCode는 201을 반환하고 유저를 생성한다.
6. 테스트 케이스 2 작성
it("이미 등록된 이메일이 있다면 error 출력", async () => {
(User.findOne as jest.Mock).mockResolvedValue({
email: "user",
// User.findOne이 값을 찾은 경우 (등록된 이메일이 있는 경우)
});
await registration(req, res, next);
// registration 비동기적으로 시행.
expect(res.statusCode).toEqual(409);
// statusCode가 409이다.
expect(res._getJSONData()).toStrictEqual({
Error: "Account already exists",
});
// json으로 오는 data가 Error: "Account already exists"이다.
});
이미 등록된 유저가 있는 경우(User.findOne이 email을 찾아서 리턴한다) statusCode는 409를 반환하고 json 데이터를 반환한다.
7. 테스트 케이스3 작성
it("유저 생성하면서 에러 발생", async () => {
const errorMessage = { message: "Error" };
// reject 메시지
(User.findOne as jest.Mock).mockResolvedValue(Promise.reject(errorMessage));
// 비동기 Promise.reject로
await registration(req, res, next);
expect(next).toHaveBeenCalledWith(errorMessage);
});
Promise.reject로 비동기적으로 돌아오는 reject의 에러를 모킹해준다.
----
프로젝트를 진행하면서 처음 test를 시작했지만 이제 jest에 어느정도 익숙해지면서 단위 테스트도 작성할 수 있게 되었다.
통합 테스트의 코드는 다 작성했기 때문에 이제 어느 정도 단위 테스트와 통합 테스트를 수행할 수 있게 되었다.
테스트 케이스를 작성하면서 개발을 하는 것은 의존성에 의한 코드 에러인지 아닌지 쉽게 판별하게 해주고, 실제 서비스 DB와 연결이 되지 않아 안전하게 로직의 검증이 가능한 이점이 있다. 이러한 이유로 이제부터는 TDD 방식으로 프로젝트를 진행하도록 한다.
Reference:
[JEST] 📚 모킹 Mocking 정리 - jest.fn / jest.mock /jest.spyOn
Mocking 원리 mocking이란 (mock = 모조품) 뜻 그대로 받아드리면 된다. 즉 테스트하고자 하는 코드가 의존하는 function이나 class에 대해 모조품을 만들어 '일단' 돌아가게 하는 것이다. 한마디로, 단위
inpa.tistory.com
GitHub - howardabrams/node-mocks-http: Mock 'http' objects for testing Express routing functions
Mock 'http' objects for testing Express routing functions - GitHub - howardabrams/node-mocks-http: Mock 'http' objects for testing Express routing functions
github.com
따라하며 배우는 TDD 개발 - 인프런 | 강의
이 강의를 통해서 테스트 주도 개발(TDD)을 이용하여 Node.js 어플리케이션을 만들어 봅니다., - 강의 소개 | 인프런...
www.inflearn.com
'Computer science > Architecture' 카테고리의 다른 글
의존성 역전의 원칙(DIP) (0) | 2023.08.07 |
---|---|
[Jest] CRUD 단위 테스트 (2) | 2023.02.04 |
[Jest] passport 로그인 단위 테스트 (0) | 2023.02.04 |