📋 目錄
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:核心差異
| 面向 | Jest | Vitest |
|---|---|---|
| 速度 | 慢(JavaScript) | 快(Go esbuild) |
| TypeScript | 需要 ts-jest | 原生支援 |
| ESM | 實驗性,需額外設定 | 原生支援 |
| 設定檔 | jest.config.js | vitest.config.ts |
| Snapshot | .snap 檔案 | .snap 檔案(相同) |
| Mock API | jest.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 的核心步驟:
- 安裝
vitest - 建立
vitest.config.ts - 把
jest.fn()改成vi.fn() - 沿用既有的 Snapshot 檔案
- 執行測試,確認行為一致
對於新專案,直接用 Vitest,不需要再從 Jest 開始。對於舊專案,可以趁 Jest 還能跑的時候逐步遷移,不需要一次全部換完。
想了解如何用 Playwright 做 E2E 測試,可參考 《Playwright E2E 測試——比 Cypress 更強的現代選擇》。