Jest를 이용한 Express 테스트
개발이 어느정도 진행되고, API 서버가 정상 작동하는지 포스트맨으로 일일히 확인해봤다.
하지만 MVP
단계이긴 하지만 규모가 점차 커질수록 기능이 많아져 수작업으로 테스트를
하는게 불안해지기 시작했다. 그래서 Jest
를 이용한 단위 테스트로 TDD를 적용해 보고자
포스팅을 하게 되었다.
👠 Jest
Jest
는 유닛 테스트, 통합 테스트, 함수 mocking 등 테스트 코드를 편히 작성할 수 있게 도와주는 프레임워크다.
❓ Jest를 사용하는 이유
lint
가 코드 스타일에 규칙을 정하는 것이라면 코드가 올바른 기능을 하는 지 체크 할 수 있다.
이를 통해 보다 안정적이고 제대로 동작하는 코드를 작성할 수 있다.
🛠 Jest 설정하기
현재 개발 환경이 TypeScript
이므로 ts-jest
, @types/~
관련 모듈도 설치해 준다.
그리고 supertest
는 request
통신을 만들어 주는 역할을 하니 express
테스트를 위해 필요하므로 같이 설치해 준다.
npm insatll --save-dev @types/jest @types/supertest jest ts-jest supertest
또는
yarn add --dev @types/jest @types/supertest jest ts-jest supertest
그 뒤 script 설정과 jest 설정을 따로 해줘야 한다.
package.json
{
"scripts": {
"test": "cross-env NODE_ENV=test jest --setupFiles dotenv/config --config jest.config.js --detectOpenHandles --forceExit"
},
}
test 환경으로 핸들링 하기 위해 NODE_ENV
를 test로 설정 한다.
env파일로 환경변수를 관리한다면 --setupFiles dotenv/config
꼭 넣어야 jest에서 환경변수를 사용할 수 있다.
Node.js
에서 실행이 모두 완료 되어도 비동기가 남아있는 경우 jest가 종료되지 않는데 detectOpenHandles
옵션으로 열려있는 핸들링 을 모두 수집하고 forceExit
옵션으로 테스트 후 종료시켜 준다.
jest.config.js
module.exports = {
preset: "ts-jest",
moduleNameMapper: {
"@routes/(.*)$": "<rootDir>/src/routes/$1",
"@controllers/(.*)$": "<rootDir>/src/controllers/$1",
"@services/(.*)$": "<rootDir>/src/services/$1",
"@repository/(.*)$": "<rootDir>/src/repository/$1",
"@entities/(.*)$": "<rootDir>/src/entities/$1",
"@modules/(.*)$": "<rootDir>/src/modules/$1",
},
};
TypeScript
환경에서 jest를 사용한다면 preset에 ts-jest
를 명시해줘야 한다.
moduleNameMapper
는 타입스크립트 환경에서 Path Alias
를 사용하는 경우 꼭 설정해 줘야 테스트 할 때 경로를 알아서 찾는다.
이로써 Jest
기본 설정은 끝이다.
🛫 실제 테스트
프로젝트 구조
├── src # 소스 폴더
├── modules # 사용자 지정 모듈
├── routes # 요청에 따른 분리
├── controllers # API 요청/응답 실행
├── services # 데이터 가공
├── repository # DB CRUD
├── entities # Model과 동일
└── test # Jest 테스팅 폴더, 이곳에 테스트 파일들이 모두 담긴다.
테스트를 하기위해 테스트 파일을 작성해야 하는데 아무 경로에 원하는이름.test.ts
이렇게 명시해주면 jest가 알아서 찾아 테스팅을 하게 된다.
- ex) 유저 테스트를 하는 경우 user.test.ts 또는 user.spec.ts
🎓 Jest 기본 규칙
describe
: 여러개의 테스트 케이스를 묶어 가독성을 높인다.
test/it
: 테스트 단위
Methods LIST : 전역에 쓰면 모든 테스트가 적용되고 묶은 describe 속에 넣으면 그 속에 있는 test에만 methods가 적용된다.
afterAll(fn, timeout)
: 모든 테스트가 끝나고 실행된다afterEach(fn, timeout)
: 하나의 테스트가 끝날 때마다 실행된다beforeAll(fn, timeout)
: 모든 테스트가 시작하기 전에 한번 실행된다.beforeEach(fn, timeout)
: 하나의 테스트가 시작하기 전에 매번 실행한다
예시
describe('테스트 단위', () => {
beforeAll(() => {
/* 모든 test() 함수 실행 이전에 작동한다. */
});
afterAll(() => {
/* test() 함수가 모두 실행된 이후에 작동한다. */
});
test('무슨 테스트인지 여기에 작성한다.', () => {
...
});
});
- describe 단위로 여러 테스트를 진행하면 관리하기 용이하다.
🎬 Jest를 이용한 테스트
app.ts
if (env.nodeEnv !== "test") {
createConnection()
.then(() => console.log("🚀 DB Connected"))
.catch((err) => console.log(err));
}
app.ts
에서 DB 연결을 시도하면 테스팅이 끝날때 까지 Typeorm connection
이 끝나지 않아서 오류가 발생한다.
app.ts에 비동기 방식으로 짜여진 핵심 코드가 더 있다면 오류 날 확률이 생길 것이다. 그래서 NODE_ENV !== "test"
인 경우만 app.ts
에서 DB 연결을 시도하고 테스팅 환경에선 테스트 파일에서 따로 DB 연결을 시도해 준다.
- app.ts에서 DB 연결을 시도한 경우 import error가 뜬다.
src/test/shop.test.ts
import request from "supertest";
import { createConnection } from "typeorm";
import app from "../app";
describe("Api Test", () => {
beforeAll(async () => {
await createConnection()
.then(() => console.log("🚀 DB Connected"))
.catch((err) => console.log(err));
});
test("모든 가게 리스트 갖고 와야함", async () => {
const response = await request(app).get("/v0/shops");
// expect에 예상 값을, toBe에 결과 값을 넣는다.
expect(response.status).toBe(200);
});
}
위 코드는 Jest Tester가 /v0/shops
URL에 GET
요청을 보낸 후 예상 값과 결과 값을 비교 해 결과를 반환해준다.
현재 프로젝트에서 위 URL로 GET
요청을 보내면 DB에서 모든 가게 리스트를 조회 해, 성공 시 Http Status Code - 200 OK
응답을 보내준다.
위 코드에서 결과 값 200과 응답 값(예상 값)이 200으로 동일해 테스트 성공이라는 결과가 나왔다.
이런식으로 단위 테스트를 진행하면 되고 나중엔 통합 테스트, 부하 테스트를 공부한 뒤 포스팅 해야겠다.