실제 회사에서 TDD를 하고있진 않지만 항상 Unit Test툴에 대한 관심은 가지고 있다. 매년 초 여느때와 마찬가지로 나는 state of JS를 보고있었고,
메타프레임워크의 순위를 훑는데 사실 테스트 프레임워크의 순위는 Jest
가 1위인채로 크게 바뀌지 않아 주의깊게 보질 않았는데. 그래프가 급격히 우상향한 프레임워크를 보게되었고 playwright
와 vitest
였다.
playwright
는 예전 E2E 테스트를 구축할때 사용을 해봤었다. Code-Generator를 통해 테스트 코드를 생성해주는 기능이 있어 편리했던 경험이 있다.
vitest
는 예전 vite로 번들러 교체를 진행했을때 살짝 훑었었고 당시 여전히 Jest
가 주였기에 크게 신경쓰지 않았지만 왠걸 사용률이 급상승하고 있었다.
어떻게 찍어먹어볼까 생각하던중. 학습차 생성했던 Jest 맛보기 레포지토리가 있는걸 떠올렸고 vitest
로 바꿔보면 좋을 것 같다는 생각이 들었다.
학습차 생성한 레포인만큼 커버리지가 적어 크게 와닿지 않을 수도 있지만, 어떤 장점이 있는지 느껴보려고 한다.🤤
왜 Vitest일까?
항상 새로운 툴을 볼때는 공식문서에 있는 본인들의 탄생 이유와 장점을 먼저 보는 편이다. vite의 설정과 동일한 환경에서 적은 의존성으로 Jest와 거의 유사한 API를 사용할 수 있는 Test-Runner라고 한다.
이러면 vite
를 사용하지 않는다면 큰 메리트가 없을 것 같이 느낄 수 있지만, 성능을 위해 Worker 스레드를 활용
하거나 테스트 병렬실행
및 vite의 HRM같은 watch mode
의 기본지원 등 DX가 좋아 vite를 사용하지 않더라도 경쟁력있는 Test-Runner라고 한다.
실제로 vite로 번들러를 변환 후 빠른 속도개선을 체감했기때문에, vite의 개발 서버를 이용한 Test-Runner라면 현재 레포지토리 에서는 커버리지가 적어 느끼진 못하겠지만 속도개선도 분명히 있을거라는 생각도 들었다.
vite.config.js
애초에 테스트설정과 기존 vite설정을 같이 쓸수있는게 장점인만큼 vitest.config
파일은 모든 vite.config의 옵션을 연장한다.
하지만 기존vite.config
을 대체하기 때문에 vite.config과 vitest.config이 별도로 존재한다면 기존 vite.config은 무시된다.
별도의 설정으로 관리하고싶으면 mergeConfig
을 이용해서 vitest.config에서 합쳐서 사용해야한다.
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config.mjs'
export default mergeConfig(viteConfig, defineConfig({
test: {
// ...add your test config
},
}))
기존 vite.config에서도 test 프로퍼티를 추가해서 관리할 수 있는데 triple slash directive
를 사용해야하며 다음 메이저버전에서 중단될 기능이라 알아만두는게 좋아보인다.
/// <reference types="vitest/config" />
import { defineConfig } from 'vite'
export default defineConfig({
test: {
// ...add your test config
},
})
기존 레포의 jest.config
을 vitest.config
으로 교체했다. 바뀐건 기존 jest.config에는 coverageProvider의 기본값이 "babel"이어서 명시적으로 "v8"을 설정했지만
vitest는 기본이 "v8"이기에 해당 옵션이 생략된 정도가 전부였고 jest의 설정이 거의다 존재하기때문에 설정이 많았어도 딱히 어려움은 없어보였다.
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
clearMocks: true,
environment: "jsdom",
dir: "./src",
setupFiles: ["./vitest.setup.ts"],
},
});
테스트 API 교체 Jest -> Vitest
이게 끝..?
교체과정에서 함수이름이 좀 다른 부분을 발견했기때문에 100%는 아니더라도 jest의 방식과 99%정도 일치해서 바꾸는데 크게 어려움은 없어서 놀라웠다!
jest는 test, describe
등 API를 전역적으로 주입해서 별도의 import없이 사용
이 가능했지만. vitest는 vite기반(ESM)이기 때문에 명시적으로 api를 import
해야해 그부분만 수정해 줬다.
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("./mock-advance2", async () => {
// 기존 jest = requireActual()
const origin = await vi.importActual("./mock-advance2");
return {
__esModule: true,
...origin,
UserClient: vi.fn(),
};
});
바로 될리가없지!
하지만 @testing-library를 이용한 DOM 테스트에서 몇개의 테스트를 통과못해 확인해보니 Found multiple elements Error
를 발견했다.
딱보니 test마다 clean-up이 제대로 안되고있는거 같아보였다. jest환경일때는 문제가 없었는데.. jest-dom/vitest
환경에서 뭔가 문제를 일으키나? 싶었다.
어쨋든 clean-up을 매 테스트마다 해줄게 아니니 setup-file
에 넣어줘서 해결했다.
// vitest.setup.ts
import "@testing-library/jest-dom/vitest";
import { cleanup } from "@testing-library/react";
import { afterEach } from "vitest";
afterEach(() => {
cleanup();
});
추가로 jest-dom
의 toHaveStyle
어설션이 문제가 있었다. 아마 jest환경에서는 통과했었는데 안되는걸보면 jest-dom/vitest의 문제다 싶었다.
어떤 로그도 뜨지않아서 애먹었는데 예상되는건 jsdom이 파싱한 엘리먼트를 jest-dom/vitest의 어설션이 읽을때 문제를 발생시키는 정도였다. 어쨋든 모든 어설션이 문제를 발생시키는게 아니라 해당부분만 약간 수정해주었다.
test("toggle - style", () => {
// before (fails ❌)
expect(element).toHaveStyle({
backgroundColor:"black",
textDecoration:"none
})
// after (success ✅)
expect(element.style.backgroundColor).toBe("black")
expect(element.style.textDecoration).toBe("none")
})
엄청 간단했던 마이그레이션! 테스트 결과는?
마이그레이션이 너무 후루룩뚝딱이라 놀랐다! 테스트 커버리지도 적기때문에 큰 결과는 없을거라 생각했지만 5.6s -> 1.48s 라는 엄청 유의미한 결과를 얻었다.
간단한 레포지토리에서도 이정도인데 큰 서비스에서는 얼마나 큰 변화일까 싶다. Vitest의 모든걸 알아보진 않았지만 간단한 마이그레이션 만으로도 왜 많이쓰는지 알 수 있던 시간이었다 🥲
참조