Server/Node.js

Jest - 테스트 환경 구축하는 방법 (Test용 DB 사용) with Setup, Teardown

JaeHoney 2022. 2. 13. 14:51
반응형

테스트 환경 구축

현재 프로젝트로 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문을 실행하게 되어있습니다.

 

그래서 스키마와 테이블구조가 올라가고 무사히 테스트를 마칠 수 있게 됩니다.

 

감사합니다.

반응형