Vitest. 가볍게 훑어 먹어보자
2025-03-24

실제 회사에서 TDD를 하고있진 않지만 항상 Unit Test툴에 대한 관심은 가지고 있다. 매년 초 여느때와 마찬가지로 나는 state of JS를 보고있었고, 메타프레임워크의 순위를 훑는데 사실 테스트 프레임워크의 순위는 Jest가 1위인채로 크게 바뀌지 않아 주의깊게 보질 않았는데. 그래프가 급격히 우상향한 프레임워크를 보게되었고 playwrightvitest였다.

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.configvitest.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-domtoHaveStyle 어설션이 문제가 있었다. 아마 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 라는 엄청 유의미한 결과를 얻었다.

변경전 - Jest
변경후 - Vitest


간단한 레포지토리에서도 이정도인데 큰 서비스에서는 얼마나 큰 변화일까 싶다. Vitest의 모든걸 알아보진 않았지만 간단한 마이그레이션 만으로도 왜 많이쓰는지 알 수 있던 시간이었다 🥲