💻 프로그래밍

Vitest

Vite-native Unit Test Framework

Vite 기반 빠른 테스트 프레임워크. Jest 호환 API로 쉬운 마이그레이션, ESM 네이티브 지원으로 빠른 실행 속도 제공.

📖 상세 설명

Vitest는 Vite 기반의 차세대 단위 테스트 프레임워크입니다. 2022년 처음 공개된 이후 빠르게 성장하여 현재 Vite 생태계의 공식 테스트 솔루션으로 자리잡았습니다. Vite의 변환 파이프라인과 설정을 그대로 재사용하여 개발 환경과 테스트 환경의 일관성을 보장합니다.

Vitest의 가장 큰 장점 중 하나는 Jest 호환 API입니다. describe, it, expect, beforeEach, afterEach 등 Jest에서 사용하던 API를 거의 그대로 사용할 수 있어, 기존 Jest 테스트 코드를 최소한의 수정으로 마이그레이션할 수 있습니다. 또한 jest.fn(), jest.mock() 같은 모킹 API도 vi.fn(), vi.mock()으로 유사하게 제공됩니다.

ESM(ECMAScript Modules) 네이티브 지원은 Vitest의 핵심 경쟁력입니다. Jest가 CommonJS 기반으로 ESM 지원에 복잡한 설정이 필요한 반면, Vitest는 Vite의 ESM 기반 개발 서버를 활용하여 별도 설정 없이 ESM을 지원합니다. 이로 인해 테스트 시작 시간이 획기적으로 단축되고, watch 모드에서의 재실행 속도도 매우 빠릅니다.

실무에서 Vitest는 HMR(Hot Module Replacement)을 활용한 즉각적인 테스트 재실행, TypeScript와 JSX의 제로 설정 지원, 멀티스레드 워커를 통한 병렬 테스트 실행 등 현대적인 개발 경험을 제공합니다. 특히 Vite 기반 프로젝트(Vue, React, Svelte 등)에서는 vite.config.ts 설정을 그대로 사용할 수 있어 설정 중복을 피할 수 있습니다.

💻 코드 예제

기본 테스트
// sum.ts
export function sum(a: number, b: number): number {
    return a + b;
}

// sum.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { sum } from './sum';

describe('sum 함수', () => {
    it('두 숫자를 더한다', () => {
        expect(sum(1, 2)).toBe(3);
    });

    it('음수도 처리한다', () => {
        expect(sum(-1, 1)).toBe(0);
        expect(sum(-1, -1)).toBe(-2);
    });

    // 테이블 기반 테스트 (test.each)
    it.each([
        [1, 1, 2],
        [2, 2, 4],
        [0, 0, 0],
    ])('sum(%i, %i) = %i', (a, b, expected) => {
        expect(sum(a, b)).toBe(expected);
    });
});
모킹 (Mocking)
// api.ts
export async function fetchUser(id: string) {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
}

// userService.ts
import { fetchUser } from './api';

export async function getUserName(id: string): Promise<string> {
    const user = await fetchUser(id);
    return user.name;
}

// userService.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getUserName } from './userService';

// 모듈 모킹
vi.mock('./api', () => ({
    fetchUser: vi.fn()
}));

import { fetchUser } from './api';

describe('getUserName', () => {
    beforeEach(() => {
        vi.clearAllMocks();
    });

    it('사용자 이름을 반환한다', async () => {
        // 모킹된 함수의 반환값 설정
        vi.mocked(fetchUser).mockResolvedValue({
            id: '1',
            name: '홍길동'
        });

        const name = await getUserName('1');

        expect(name).toBe('홍길동');
        expect(fetchUser).toHaveBeenCalledWith('1');
        expect(fetchUser).toHaveBeenCalledTimes(1);
    });

    it('API 에러 시 예외를 던진다', async () => {
        vi.mocked(fetchUser).mockRejectedValue(new Error('Network Error'));

        await expect(getUserName('1')).rejects.toThrow('Network Error');
    });
});
스냅샷 테스트
// component.test.ts
import { describe, it, expect } from 'vitest';

// 객체 스냅샷
describe('스냅샷 테스트', () => {
    it('사용자 객체 스냅샷', () => {
        const user = {
            id: 1,
            name: '홍길동',
            email: 'hong@example.com',
            roles: ['admin', 'user'],
            createdAt: new Date('2024-01-01')
        };

        // 첫 실행 시 스냅샷 생성, 이후 비교
        expect(user).toMatchSnapshot();
    });

    it('인라인 스냅샷', () => {
        const config = {
            theme: 'dark',
            language: 'ko'
        };

        // 인라인 스냅샷 - 코드에 직접 저장
        expect(config).toMatchInlineSnapshot(`
          {
            "language": "ko",
            "theme": "dark",
          }
        `);
    });

    // React 컴포넌트 스냅샷 (with @testing-library/react)
    it('React 컴포넌트 렌더링 스냅샷', () => {
        const html = '<button class="btn-primary">제출</button>';
        expect(html).toMatchSnapshot();
    });
});

// vitest.config.ts 설정 예시
// export default defineConfig({
//     test: {
//         snapshotFormat: {
//             printBasicPrototype: false
//         }
//     }
// });

🗣️ 실무 대화 예시

팀 회의에서 Jest에서 Vitest 전환 논의 중

"Jest에서 Vitest로 전환하면 테스트 실행 시간이 절반 이하로 줄어요. 우리 프로젝트가 이미 Vite 기반이라 vite.config.ts를 그대로 쓸 수 있고, Jest API와 거의 동일해서 마이그레이션도 간단합니다. vi.mock()으로 바꾸고 import 경로만 vitest로 바꾸면 대부분 동작해요."

CI/CD 파이프라인 최적화 회의에서

"GitHub Actions에서 Vitest 실행할 때 --reporter=junit 옵션으로 JUnit 리포트 생성하고, --coverage로 커버리지 리포트 뽑으면 됩니다. 병렬 실행은 기본이고, --pool=threads 옵션으로 워커 풀 방식도 선택할 수 있어요. CI에서는 --run 플래그로 watch 모드 비활성화하는 것 잊지 마세요."

기술 면접에서

"Vitest가 Jest보다 빠른 이유는 Vite의 ESM 기반 개발 서버를 활용하기 때문입니다. Jest는 CommonJS로 트랜스파일 후 테스트하는데, Vitest는 ESM을 네이티브로 처리해서 번들링 오버헤드가 없어요. 그리고 HMR을 활용해서 변경된 파일만 재실행하니까 watch 모드가 특히 빠릅니다."

코드 리뷰에서 - 테스트 코드 개선

"이 테스트에서 vi.mock() 호출이 각 테스트 케이스마다 반복되고 있네요. 파일 상단에서 한 번만 호출하고 beforeEach에서 vi.clearAllMocks()로 초기화하는 게 깔끔해요. 그리고 vi.mocked(fetchUser).mockResolvedValue()에서 타입 추론이 잘 되는지 확인해보세요. 안 되면 vi.mocked로 제네릭 명시해주세요. 마지막으로 비동기 테스트에서 await expect().rejects.toThrow() 패턴 쓸 때 반드시 await 붙이는 거 잊지 마세요."

⚠️ 주의사항

1
Jest와의 미묘한 차이점

대부분의 Jest API가 호환되지만, 일부 차이가 있습니다. jest.fn()은 vi.fn()으로, jest.mock()은 vi.mock()으로 변경해야 합니다. 또한 vi.mock()은 파일 상단으로 호이스팅되므로 동적 모킹이 필요하면 vi.doMock()을 사용하세요. 타이머 관련 API도 미세한 동작 차이가 있을 수 있습니다.

2
브라우저 테스트 설정

Vitest는 기본적으로 Node.js 환경에서 실행됩니다. DOM 테스트가 필요하면 vitest.config.ts에서 environment: 'jsdom' 또는 'happy-dom'을 설정해야 합니다. 실제 브라우저 테스트가 필요하면 @vitest/browser 패키지와 함께 Playwright나 WebdriverIO를 사용하세요.

3
커버리지 설정

커버리지 리포트를 위해서는 @vitest/coverage-v8 또는 @vitest/coverage-istanbul 패키지를 별도 설치해야 합니다. v8이 더 빠르지만, istanbul이 더 정확한 결과를 제공할 수 있습니다. CI 환경에서는 coverage.reporter에 ['text', 'json', 'html']을 설정하여 다양한 형식의 리포트를 생성하세요.

🔗 관련 용어

📚 더 배우기