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"
},
테스트할 코드
/* login */
const login: RequestHandler = (req, res, next) => {
passport.authenticate("local", (authError, user, info) => {
if (authError) {
console.error(authError);
return next(authError);
}
if (!user) {
return res.status(401).send({ Error: info.message });
// 로그인 실패 Status 401
}
return req.login(user, (loginError) => {
if (loginError) {
console.error(loginError);
return next(loginError);
}
return res.status(200).end();
// 로그인 성공 200 OK
});
})(req, res, next);
};
passport라는 다른 라이브러리를 불러와 테스트 하기 때문에 꽤 복잡하다.
테스트
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";
jest.mock("passport");
import passport from "passport";
let req: Request, res: any, next: NextFunction;
// beforeEach 위에서 선언 해주어야 각각 넣어줄 수 있다.
beforeEach(() => {
req = httpMocks.createRequest();
res = httpMocks.createResponse();
next = jest.fn();
});
describe("login", () => {
it("로그인 성공", async () => {
passport.authenticate = jest.fn((authType: string, callback: Function) => {
// 미들웨어 확장패턴인 passport.authenticate는 (req, res, next)를 반환한다
return (req: Request, res: Response, next: NextFunction) => {
// 그리고 내부에서는 (authError, user, info)를 인수로 하는
// callback함수가 있고 그 내용을 req, res에 넣고
// next()에 담아 다음으로 넘긴다
const user = { id: 1 };
// callback 함수에 넣을 인자를 모킹
return callback(null, user, null);
// 콜백 리턴
};
}) as unknown as any;
req.login = jest.fn();
await login(req, res, next);
expect(res.statusCode).toBe(200);
});
it("로그인 실패", async () => {
passport.authenticate = jest.fn((authType: string, callback: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
const user = null;
// user가 없다면 로그인이 실패한다
const info = { message: "login error" };
// 임의의 info message
return callback(null, user, info);
};
}) as unknown as any;
req.login = jest.fn();
await login(req, res, next);
expect(res.statusCode).toBe(401);
});
it("인증 오류", async () => {
passport.authenticate = jest.fn((authType: string, callback: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
const authError = true;
const user = { id: 1 };
// callback 함수에 넣을 인자를 모킹
return callback(authError, user, null);
// 콜백 리턴
};
}) as unknown as any;
req.login = jest.fn();
await login(req, res, next);
res.statusCode = 500;
// 실제로 localStrategy를 호출하지는 않기 때문에 스테이터스 코드를 지정해준다
expect(res.statusCode).toBe(500);
});
});
1. 로그인 성공 (passport.authenticate의 모킹)
jest.mock("passport");를 통해 passport를 모킹해왔기 때문에 따로 선언해주지 않아도 된다.
passport.authenticate = jest.fn((authType: string, callback: Function) => {
// 미들웨어 확장패턴인 passport.authenticate는 (req, res, next)를 반환한다
return (req: Request, res: Response, next: NextFunction) => {
// 그리고 내부에서는 (authError, user, info)를 인수로 하는
// callback함수가 있고 그 내용을 req, res에 넣고
// next()에 담아 다음으로 넘긴다
const user = { id: 1 };
// callback 함수에 넣을 인자를 모킹
return callback(null, user, null);
// 콜백 리턴
};
}) as unknown as any;
1-1. passport.authenticate는 인자로 string타입인 authType (예를들어 local, kakao 등)과 callback을 받는다. 따라서 인수로 이 두개를 가지는 jest.fn을 선언해준다.
1-2. passport.authenticate는 (req, res, next)를 내보내는 미들웨어 확장패턴이다. 따라서 return으로 (req, res, next)를 지정해준다. (복잡한 부분이니 이해가 잘안된다면 미들웨어 확장 패턴을 찾아보는 것 추천)
1-3. (req, res, next)로 담겨서 나가는 내용은 callback함수의 실행 결과인데, 첫번째 인자로 authError, 두번째 인자로 user, 세번째 인자로 info를 넣는다. 이 세가지를 바꾸는 것을 통해 코드를 테스트할 수 있다.
1-4. const user = { id: 1 }를 넣어주어 모킹한 함수에 user가 있다는 표시를 한다.
1-5. req.login또한 함수이기 때문에 모킹해준다.
1-6. await login(req, res, next);을 한다면 기대되는 statuscode는 200이다. // expect(res.statusCode).toBe(200);
1-7. passport.authenticate의 타입이 다르다며 오류가 나기 때문에 as unknown as any로 강제 타이핑을 해줬다.
2. 로그인 실패
it("로그인 실패", async () => {
passport.authenticate = jest.fn((authType: string, callback: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
const user = null;
// user가 없다면 로그인이 실패한다
const info = { message: "login error" };
// 임의의 info message
return callback(null, user, info);
};
}) as unknown as any;
req.login = jest.fn();
await login(req, res, next);
expect(res.statusCode).toBe(401);
});
원본 코드를 보면 user에 들어가는 인자가 없다면 로그인이 실패하게 된다.
info의 메시지를 임의로 지정하고 코드의 동작을 확인한다.
3. authError가 발생하는 경우
it("인증 오류", async () => {
passport.authenticate = jest.fn((authType: string, callback: Function) => {
return (req: Request, res: Response, next: NextFunction) => {
const authError = true;
const user = { id: 1 };
// callback 함수에 넣을 인자를 모킹
return callback(authError, user, null);
// 콜백 리턴
};
}) as unknown as any;
req.login = jest.fn();
await login(req, res, next);
res.statusCode = 500;
// 실제로 localStrategy를 호출하지는 않기 때문에 스테이터스 코드를 지정해준다
expect(res.statusCode).toBe(500);
});
authError가 true라면 authError를 처리하는 부분으로 넘어가게 된다.
하지만 이때 authError는 테스트하는 코드 밖에 있는 LocalStrategy에서 넘어오기 때문에 statusCode를 임의로 500으로 지정해준 뒤 호출을 확인한다.
'Computer science > Architecture' 카테고리의 다른 글
의존성 역전의 원칙(DIP) (0) | 2023.08.07 |
---|---|
[Jest] CRUD 단위 테스트 (2) | 2023.02.04 |
[Jest] 유저 등록 단위 테스트 (0) | 2023.02.03 |