개발중인 프로젝트에서 jest 를 통해 테스트코드를 실행하고있는데, 실행하는 속도가 오래걸리는것을 확인했다.
테스트코드는 아주 자주 많이 실행되어야하는데, 이 시간이 오래걸린다면 개발 생산성에 아주 큰 걸림돌이 될 수 있다.
그래서 이를 더 빠르게 개선해볼 수 없을까 고민하며 실험해본 내용을 기록한다.
jest 에서 --coveage
옵션 적용 시 JavaScript heap out of memory
가 발생하고 있다.
어디선가 메모리 누수가 발생하고 있다고 생각했다.
토스에서는 커버리지 80%로 약 1400개의 테스트가 실행되고있고, 이 테스트가 1분이 넘어가서 리팩토링을 했다고 한다.
(개선 후 2334개의 테스트 케이스가 6s 255ms 만에 끝남 - 맥북프로 i9 기준)
개발중인 프로젝트에서 테스트를 실행해보면
956개 테스트가 164초 약 2분 44초가 넘게 걸리고 있었다. (로컬 mac 기준)
결론적으로 잘못 실행하고 있으며 오래 실행되고 있다고 판단했다.
"clearMocks": true,
"resetMocks": true,
(실행 속도 차이는 없다.)
이 옵션은 각 spec.ts 에서 mock clear하는작업을 자동으로 처리해준다.
beforeEach(() => {
jest.clearAllMocks();
}); // 위 설정으로 인해 필요하지 않음
--logHeapUsage
을 통해 메모리 누수 확인해봤지만, 특별한 이상 없었다.--runInBand
옵션을 사용하고있다.runInBand 는 JEST_WORKER_ID
를 1개만 설정한다. 그래서 모든 spec.ts 를 순차적으로 실행한다.
시간을 오래걸리게 하는 가장 큰 원인이었다.
Jest 공식문서에서는 이 옵션은 디버깅을 위해 사용할것을 제안하고있었다.
--runInBand
옵션 제거무수히 많은 에러 발생했지만 실행시간은 38초로 매우 빨라지는걸 볼 수 있었다.
원인으로는 모든 TC가 순차적으로 실행하지 않고 병렬로 실행하면서 속도는 엄청 올라갔고, tc가 실행되다가 fail로 인해 중간에 멈췄기 때문이다.
다만 동시에 실행하다보니 DB 데이터를 동시에 접근하면서 TC 실패 발생한다.
(서로의 TC에 영향을 주고있다. 원인은 db를 공유하기 때문)
jest가 실행될 때 node.env.JEST_WORKER_ID 를 통해 본인의 id 를 알 수 있다.
(1번 worker가 끝나면 다음 spec.ts 가 1번 worker에 할당되는 방식)
최대 개수는 PC에서 사용가능한 코어수 - 1 (메인스레드) 입니다.
const databaseName = `shop_test_${node.env.JEST_WORKER_ID}`;
jest_worker_id 별로 디비를 생성했다.
예를들면
foo_test_1
foo_test_2
...
bar_test_1
bar_test_2
...
TC의 설정 부족으로 여전히 failed 하는 tc는 존재하지만 63초로 시간은 많이 줄어들었다.
생성할 DB는 5개이다.
예를들면 foo, bar, qaz, qwe, asd
tc마다 timeout을 5000 으로 주고있는데, 디비 세팅하다가 이 시간이 초과되기도 한다.
ex) foo_test → foo_test_1 복사
지금은 knex에서 제공하는 migrations 기능을 사용하고 있다.
이걸 최초 1번만 사용해 sql 덤프파일을 만들고 그 파일을 재사용하면 빨라지지않을까 생각해봤다.
처음에 foo_test 에 대한 dump 파일을 생성하고, foo_test_1, foo_test_2.. 는 이 덤프파일을 이용하는 것이다.
원인으로는 knex 에 연결할 때 connection pool 을 10개씩 주고있는데, jest worker 마다 새로운 연결을 만들다보니 worker * 10 개씩 연결하고 있다.
mysql connection pool 조절하여 해결해줬다.
test 환경에서 1로 설정
현재 failed 되는 코드는 잘못 작성된 코드로 판단하여 TC 수정했다.
그러면서 위에서 나왔던 메모리 누수도 사라졌다..
아마도 메모리 누수는 외부 서버 호출에 대해 mocking 하면서 사라졌을거라 추측한다.
fooTestModule.setFixtures(fooFixtures);
barTestModule.setFixtures(barFixtures);
테스트가 실행되기 전 (beforeEach) fixture를 세팅하는데, 특정 디비를 세팅할 때마다 모든 디비를 세팅하고있기 때문이었다.
setFixtures 함수 안에 보면 전체 디비를 구성하는 로직이 들어있다.
이를 해결하기위해 setFitures에서 디비 세팅하는 로직을 제거하였다.
혹은 테스트를 실행할 때 JEST_WORKER_ID 개수만큼 생성할 수 없을까?
jest 의 setupFiles을 사용하면 jest 를 실행할 때 최초 1회 실행하고싶은 코드를 구성할 수 있다.
// setupTests.js
// 최초 1회 실행할 코드를 작성
console.log("최초 1회 실행되는 코드");
// jest.config.js
module.exports = {
// 다른 Jest 설정...
setupFiles: ['./setupTests.js'],
};
문제는 여기서는 jest worker 의 개수를 알 수 없어서 다른 방법이 필요했다.
각 worker는 개별로 실행되기때문에 어떠한 값을 공유할 공간이 없다.
(공유할 데이터 공간을 만들기위해 별도의 인프라를 넣을 순 없다..)
mysql 쿼리를 사용해서 디비의 존재 여부를 확인하면 해결할 수 있었다.
MYSQL_PWD="root" mysql --user=root --host=127.0.0.1 --port=3306 -e "use ${dbname}"`
164s → 41s 약 1/4 로 감소 (로컬 mac 기준)
@swc/jest
를 사용하면 20배 더 빠른 빌드가 가능하다. (이건 다음 글을 통해 소개해본다)