테스트 환경 구축
현재 프로젝트로 Typescript + NodeJS + Express + MySQL을 이용해서 API를 개발하고 있었습니다.
문제는, 테스트 코드를 작성하는데, 문제가 Java Spring은 H2라는 인메모리 DB가 있는데 NodeJS에서는 사용할 수 있는 라이브러리가 딱히 없었습니다. SqLite가 있지만, Mysql과 규격이 다른 부분이 많아서 정확하게 테스트하기가 힘들었습니다.
그래서 MySQL 서버를 유지할 수 있는 다른 방법을 생각했습니다.
[방법 1] Docker compose를 이용해서, 테스트가 진행 시 마다 컨테이너를 down, up 해서 덤프를 생성한 후 테스트
[방법 2] 테스트 전체 실행 전에 데이터를 전부 truncate 후 덤프를 생성
[방법 3] Setup and teardown
하나씩 살펴보겠습니다!
[방법 1]
1번 방법은 Docker compose를 이용해서 테스트 환경마다 컨테이너를 down, up하는 방법입니다.
docker-entrypoint-initdb.d 라는 폴더에 스키마 생성과 테스트용 데이터를 삽입하는 .sql 파일을 넣어서 컨테이너가 생성될 때마다 테스트용 데이터를 새로 가지게 됩니다.
version: "3.1"
services:
master:
container_name: booking-test-db
image: mysql:5.7
volumes:
- ./dbsetting:/etc/mysql
- ./mysql-dump/master:/docker-entrypoint-initdb.d/
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: booking
MYSQL_USER: test
MYSQL_PASSWORD: test
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
ports:
- "3306:3306"
restart: always
command:
- --character-set-server=utf8
- --collation-server=utf8_general_ci
해당 방법으로 구현했었는데, 문제는 Jest가 실행 시 docker compose를 down & up 해주는 스크립트를 실행해야하는데, 컨테이너가 뜨는 시점이랑 테스트가 돌아가는 시점이랑 불일치가 계속 발생합니다.
그리고, 사실 단위테스트는 각 테스트마다 데이터를 초기화해주는게 맞는거라서, 컨테이너를 몇번이고 새로 띄워야하기 때문에 적합하지 않다고 판단했습니다.
[방법 2]
2번 방법은 테스트가 실행 시에 SQL 문을 실행시켜주어, 데이터를 초기화 후 dump 데이터를 세팅하는 방법입니다.
const sqlRun = (testDB: Sequelize): any => {
const contents = fs.readFileSync(__dirname + "/dump-datas.sql", "utf-8");
const promises = [];
const statements = contents.toString().split(";");
for (const statement of statements)
if (statement.trim() != "") promises.push(testDB.query(statement));
return Promise.all(promises);
};
해당 방법도 나쁜 방법은 아니었지만, 이런 식으로 전체 덤프 데이터를 백업하면, A를 테스트하는데도, B와 C 관련 데이터도 세팅해야해서 성능도 낭비되고, sql문과 해당 sql문을 읽는 코드에 의존적이게 되어서 책임이 모호하게 됩니다.
[방법 3]
그래서 위의 문제를 해결하기 위해 Jest의 Setup & teardown을 사용하게 되었습니다.
describe("Category controller test", () => {
afterEach(async() => {
await Category.truncate();
})
describe("changeOrders", () => {
it("withIdAndSort_200", async () => {
const category_1 = await generateCategory_sortIs200();
const category_2 = await generateCategory_sortIs200();
const changeOrdersDto: UpdateCategoryOrdersDto = {
ids: [
category_1.no.toString(),
category_2.no.toString()
]
}
const response = await request(app)
.post("/categories/change-order")
.set("x-hiworks-jwt", adminJwt)
.set("authorization-type", "user")
.send(changeOrdersDto);
expect(response.statusCode).toBe(200);
expect(response.body.data[0].sort).toBe(1);
expect(response.body.data[1].sort).toBe(2);
return;
});
});
});
Jest는 Setup and Teardown이라는 기능을 제공하는데, 특정 범위의 테스트 전 후에 원하는 코드들을 삽입할 수 있습니다.
- beforeEach -> 각각의 테스트 메서드가 실행되기 전에 실행
- afterEach -> 각각의 테스트 메서드가 종료된 후에 실행
- beforeAll -> 테스트가 몇개가 호출되더라도 실행되기 전에 1번 실행
- afterAll -> 테스트가 몇개가 호출되더라도 종료된 후에 1번 실행
저의 경우 테스트코드를 실행할 때 사용할 데이터를 생성(generate)합니다. 이 코드는 beforeEach에 포함되도 되지만, 그렇게 하면 테스트 메서드 내부에서 해당 데이터 참조가 애매해져서 각 메서드에서 세팅했습니다.
그리고, 테스트가 완료되면 afterEach에서 truncate해서 데이터를 비워줍니다.
이 방식을 사용하면, 각 테스트를 실행할 때 정말 필요한 데이터만 세팅하고, 그 데이터들만 참조할 수 있기때문에 더 safe하게 테스트를 작성할 수 있습니다.
[추가 - 배포 시 컨테이너 설정(스키마, 테이블 구조 생성)]
위 방식을 사용하면 로컬에서 테스트를 실행하기 좋은 환경을 갖출 수 있습니다.
추가로 실제 서버에 배포를 하게 되면 배포 환경에서 테스트가 돌아갈 텐데, 그 때 MySQL서버를 띄우고 스키마를 삽입해줘야 합니다.
데이터를 삽입하고 삭제하는 로직은 있어도, 스키마를 테스트 실행 시 생성하진 않으니까요!
version: "3.1"
services:
master:
container_name: booking-test-db
image: mysql:5.7
volumes:
- ./dbsetting:/etc/mysql
- ./mysql-dump/master:/docker-entrypoint-initdb.d/
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: booking
MYSQL_USER: test
MYSQL_PASSWORD: test
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
ports:
- "3306:3306"
restart: always
command:
- --character-set-server=utf8
- --collation-server=utf8_general_ci
그래서 테스트를 돌아갈 때 docker compose를 이용해서 mysql container를 띄우고, 파일이름.sql파일을 생성해서, 데이터베이스 스키마와 테이블 껍데기를 생성하는 쿼리문을 작성해야 합니다.
그래야 테스트용 데이터를 삽입하고 조회하면서 테스트할 수 있을테니까요!
그리고 volumes에 <sql파일 로컬위치>:/docker-entrypoint-initdb.d/ 를 넣어주면 됩니다.
그러면 컨테이너가 실행될 때 sql파일이 /docker-entrypoint-initdb.d 폴더에 들어가게 되고, MySQL이미지는 컨테이너가 올라갈 때 /docker-entrypoint-initdb.d폴더 안에 있는 sql문을 실행하게 되어있습니다.
그래서 스키마와 테이블구조가 올라가고 무사히 테스트를 마칠 수 있게 됩니다.
감사합니다.
'Server > Node.js' 카테고리의 다른 글
Jest - 테스트 코드간 충돌, 간섭 막는 방법 (매번 테스트 결과가 다를 때 해결 방법!) (0) | 2022.02.28 |
---|---|
NodeJS - 권한 제어 (Endpoint별 인증, 인가) 하는 법! (+ middleware) (0) | 2022.02.13 |
Sequelize - bigint를 사용하는 방법? (+ underscored) (0) | 2022.01.11 |
Sequelize - 커스텀 메서드 (Custom method) 구현 (0) | 2022.01.08 |
Sequelize - 일괄 등록, 일괄 수정 하기 (bulk create, bulk update) (0) | 2022.01.05 |