VitestJest測試Vite前端工程師前端教學

Vitest 教學:前端工程師的單元測試入門指南

Jest 遷移 Vitest 怎麼做?完整教學含配置、測試撰寫、Mock 實戰。Vite 原生支援,速度更快,打造更可靠的測試流程。

· 5 分鐘閱讀

Jest 是 2020 年之前的標準答案,但 2024 年之後越來越多新專案選擇 Vitest——同樣的 API、更快的速度、天然的 TypeScript 支援。這篇整理從 Jest 遷移到 Vitest 的完整步驟,以及 Vitest 的核心使用方式。


前言:為什麼要從 Jest 遷移到 Vitest?

Jest 的問題在 2024 年後越來越明顯:

  • 速度慢:Jest 跑一次完整測試有時需要 5-10 分鐘,大型專案特別明顯
  • TypeScript 設定麻煩:需要額外裝 ts-jest,設定瑣碎
  • ESM 支援不完整:與 Vite 的 ESM 架構有摩擦

Vitest 的核心優勢:

  • 極速:底層用 Go 的 esbuild,Jest 的 10-30 倍快
  • 原生 TypeScript:不需要 ts-jest,也不需要額外設定
  • 零設定:只要你的專案用 Vite,Vitest 可以無痛接入
  • 相同的 API:用過 Jest 的人可以無痛轉移

Jest vs Vitest:核心差異

面向JestVitest
速度慢(JavaScript)快(Go esbuild)
TypeScript需要 ts-jest原生支援
ESM實驗性,需額外設定原生支援
設定檔jest.config.jsvitest.config.ts
Snapshot.snap 檔案.snap 檔案(相同)
Mock APIjest.fn(), jest.spyOn()vi.fn(), vi.spyOn()

最大的差異是 API 名稱jest.fn() 變成 vi.fn()jest.mock() 變成 vi.mock()——其餘 API 幾乎完全相同。


從 Jest 遷移到 Vitest

Step 1:安裝 Vitest

npm install -D vitest @vitest/ui

Step 2:設定 vitest.config.ts

// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,          // 與 Jest 一樣,全域 describe/it/expect
    environment: 'jsdom',    // React 元件測試需要 jsdom
    setupFiles: ['./src/setupTests.ts'],  // 對應 Jest 的 setupFilesAfterFramework
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
});

Step 3:把 jest.config.js 裡的設定翻譯過來

// jest.config.js(舊)
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterFramework: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
    '\\.css$': 'identity-obj-proxy',
  },
  transform: {
    '^.+\\.tsx?$': ['ts-jest', { tsconfig: 'tsconfig.json' }],
  },
};
// vitest.config.ts(新)
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/setupTests.ts'],
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});

Step 4:修改 package.json 的 scripts

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:coverage": "vitest run --coverage",
    "test:watch": "vitest --watch"
  }
}

測試檔案的命名與結構

Vitest 預設會找到 *.test.ts*.spec.ts 結尾的檔案。建議兩種都支援:

src/
├── setupTests.ts
├── utils/
│   ├── formatCurrency.test.ts    # 工具函式測試
│   └── formatCurrency.ts
└── components/
    ├── Button.test.tsx           # 元件測試
    └── Button.tsx

Vitest 實務測試撰寫

工具函式測試

// src/utils/formatCurrency.ts
export function formatCurrency(amount: number, currency = 'TWD'): string {
  return new Intl.NumberFormat('zh-TW', {
    style: 'currency',
    currency,
    minimumFractionDigits: 0,
  }).format(amount);
}
// src/utils/formatCurrency.test.ts
import { describe, it, expect } from 'vitest';
import { formatCurrency } from './formatCurrency';

describe('formatCurrency', () => {
  it('should format TWD correctly', () => {
    expect(formatCurrency(1000)).toBe('NT$1,000');
    expect(formatCurrency(0)).toBe('NT$0');
    expect(formatCurrency(999999)).toBe('NT$999,999');
  });

  it('should handle different currencies', () => {
    expect(formatCurrency(100, 'USD')).toBe('$100.00');
    expect(formatCurrency(100, 'JPY')).toBe('¥100');
  });

  it('should handle negative amounts', () => {
    expect(formatCurrency(-500)).toBe('NT$-500');
  });
});

Mock 函式

// 與 Jest 幾乎相同,只有前綴從 jest 變成 vi
import { describe, it, expect, vi } from 'vitest';
import { fetchUserData } from './api';
import axios from 'axios';

vi.mock('axios');  // Mock 整個模組

describe('fetchUserData', () => {
  it('should return user data on success', async () => {
    const mockUser = { id: 1, name: '小明', email: 'xiaoming@example.com' };
    vi.mocked(axios.get).mockResolvedValue({ data: mockUser });

    const result = await fetchUserData(1);

    expect(result).toEqual(mockUser);
    expect(axios.get).toHaveBeenCalledWith('/api/users/1');
  });

  it('should throw error on failure', async () => {
    vi.mocked(axios.get).mockRejectedValue(new Error('Network error'));

    await expect(fetchUserData(1)).rejects.toThrow('Network error');
  });
});

异步測試

import { describe, it, expect, vi } from 'vitest';

describe('async utilities', () => {
  it('should handle async/await', async () => {
    const fn = vi.fn().mockResolvedValue('done');
    const result = await fn();
    expect(result).toBe('done');
  });

  it('should handle Promise.all', async () => {
    const promises = [
      Promise.resolve(1),
      Promise.resolve(2),
      Promise.resolve(3),
    ];
    const results = await Promise.all(promises);
    expect(results).toEqual([1, 2, 3]);
  });
});

Snapshot 測試

import { describe, it, expect } from 'vitest';
import { render } from '@testing-library/react';
import { Button } from './Button';

describe('Button', () => {
  it('should match snapshot', () => {
    const { container } = render(<Button>Click me</Button>);
    expect(container.firstChild).toMatchSnapshot();
  });
});

Snapshot 檔案 .snap 存在同目錄下,格式與 Jest 完全相同,可以直接沿用舊有的 snapshot。


Vitest + React Testing Library 實作

// src/components/Button.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';

describe('Button Component', () => {
  it('should render with correct text', () => {
    render(<Button>送出</Button>);
    expect(screen.getByRole('button', { name: '送出' })).toBeInTheDocument();
  });

  it('should call onClick when clicked', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click me</Button>);

    fireEvent.click(screen.getByRole('button'));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('should be disabled when disabled prop is true', () => {
    const handleClick = vi.fn();
    render(<Button disabled onClick={handleClick}>Disabled</Button>);

    const button = screen.getByRole('button');
    expect(button).toBeDisabled();

    fireEvent.click(button);
    expect(handleClick).not.toHaveBeenCalled();
  });
});

常見問題

Q:Vitest 可以直接用 Jest 的 Snapshot 檔案嗎?

A:可以。Vitest 產生的 .snap 檔案格式與 Jest 完全相同,直接複製過來就可以使用,不需要重新生成。

Q:Vitest 穩定了嗎?可以用在生產環境嗎?

A:Vitest 從 1.x 版本就已經是穩定版,Vite 生態系的核心工具之一。Vite、Radix UI、TanStack Query 等大型開源專案都在用 Vitest,完全可以在生產環境使用

Q:Vitest 有Coverage 嗎?

A:有。Vitest 支援 V8 作為 coverage provider(內建),只需要加 --coverage flag 就可以生成覆�率報告:

vitest run --coverage

Q:Vitest 和 Jest 可以同時存在嗎?

A:可以,但不建議。同一個專案用兩套測試框架會造成維護負擔。建議一次遷移完畢,然後移除 Jest 的所有相關設定。


總結:Vitest 是 2026 年的新標準

Vitest 的價值主張很清楚:同樣的 API,更快的速度,更好的 TypeScript 支援

從 Jest 遷移到 Vitest 的核心步驟:

  1. 安裝 vitest
  2. 建立 vitest.config.ts
  3. jest.fn() 改成 vi.fn()
  4. 沿用既有的 Snapshot 檔案
  5. 執行測試,確認行為一致

對於新專案,直接用 Vitest,不需要再從 Jest 開始。對於舊專案,可以趁 Jest 還能跑的時候逐步遷移,不需要一次全部換完。


想了解如何用 Playwright 做 E2E 測試,可參考 《Playwright E2E 測試——比 Cypress 更強的現代選擇》