「為什麼我的測試總是跑不過?明明程式碼都沒問題啊!」經過一番調查,發現是因為他的測試環境設置不一致。這讓我想到,如果能在測試的關鍵時刻自動執行一些設置或清理工作,是不是就能避免這種問題了?
今天我們要學習 Vitest 的測試生命週期 Hook 功能,確保測試環境的一致性!
基礎篇 Kata 篇 框架篇
Day 1-10 Day 11-17 Day 18-24 ← 我們在這裡!
[=====] [=====] [=====>]
測試生命週期管理
├── 🔄 beforeEach/afterEach
├── 📦 beforeAll/afterAll
├── ⚡ 測試環境設置
└── 🎯 測試資源管理
測試生命週期 Hook 是在測試執行的特定時機點自動觸發的程式碼。它們幫助我們管理測試環境,確保每個測試都在相同的條件下執行。
Vitest 提供了生命週期 Hook 來管理測試環境。
import { describe, it, expect, beforeEach, afterEach, beforeAll, afterAll } from 'vitest';
let testData: any = {};
let startTime: number;
beforeAll(() => {
// 整個測試檔案開始前執行一次
console.log('📦 開始測試套件');
});
afterAll(() => {
// 整個測試檔案結束後執行一次
console.log('✅ 測試套件完成');
});
beforeEach(() => {
// 每個測試前執行
startTime = Date.now();
testData = { value: 0 };
});
afterEach(() => {
// 每個測試後執行
const duration = Date.now() - startTime;
console.log(`⏱️ 執行時間: ${duration}ms`);
testData = {};
});
describe('lifecycle hooks', () => {
it('test_with_fresh_data', () => {
expect(testData.value).toBe(0);
testData.value = 100;
expect(testData.value).toBe(100);
});
it('test_data_is_reset', () => {
// 因為 beforeEach,資料重置了
expect(testData.value).toBe(0);
});
});
讓我們使用 Hook 來管理測試資料。
import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
interface User {
id: number;
name: string;
email: string;
}
let testDatabase: User[] = [];
const setupTestDatabase = () => {
// 初始化測試資料庫
testDatabase = [];
};
const seedTestData = () => {
testDatabase.push(
{ id: 1, name: 'User 1', email: 'test1@example.com' },
{ id: 2, name: 'User 2', email: 'test2@example.com' }
);
};
const cleanupTestData = () => {
testDatabase = [];
};
describe('database hooks', () => {
beforeAll(() => {
setupTestDatabase();
});
beforeEach(() => {
seedTestData();
});
afterEach(() => {
cleanupTestData();
});
it('can_query_test_users', () => {
expect(testDatabase).toHaveLength(2);
expect(testDatabase[0].name).toBe('User 1');
});
it('each_test_has_fresh_data', () => {
// 因為 afterEach清理了資料,這裡又是全新的2筆資料
expect(testDatabase).toHaveLength(2);
});
});
import { describe, it, expect, beforeEach } from 'vitest';
interface TestContext {
isAdmin: boolean;
permissions: string[];
}
let context: TestContext;
describe('conditional hooks', () => {
beforeEach((test) => {
// 根據測試名稱決定設置
const testName = test.task.name;
if (testName.includes('admin')) {
context = {
isAdmin: true,
permissions: ['create', 'read', 'update', 'delete']
};
} else {
context = {
isAdmin: false,
permissions: ['read']
};
}
});
it('admin_can_perform_all_actions', () => {
expect(context.isAdmin).toBe(true);
expect(context.permissions).toContain('delete');
});
it('user_has_limited_permissions', () => {
expect(context.isAdmin).toBe(false);
expect(context.permissions).not.toContain('delete');
});
});
// 建立 tests/day24/apiHook.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
interface ApiContext {
user: { id: string; name: string; email: string; } | null;
token: string | null;
}
let apiContext: ApiContext = { user: null, token: null };
const setupApiUser = () => {
apiContext.user = {
id: '1',
name: 'API Test User',
email: 'api-test@example.com'
};
apiContext.token = 'test-token-123';
};
const cleanupApiUser = () => {
apiContext = { user: null, token: null };
};
describe('api authentication hooks', () => {
beforeEach(() => {
setupApiUser();
});
afterEach(() => {
cleanupApiUser();
});
it('has_authenticated_user', () => {
expect(apiContext.user).toBeTruthy();
expect(apiContext.user?.email).toBe('api-test@example.com');
expect(apiContext.token).toBe('test-token-123');
});
});
import { beforeEach, afterEach } from 'vitest';
// 全域設置 - 所有測試都會執行
beforeEach(() => {
// 清除 localStorage
localStorage.clear();
// 清除 sessionStorage
sessionStorage.clear();
// 重置 console mock
console.log = vi.fn();
});
afterEach(() => {
// 清理 DOM
document.body.innerHTML = '';
// 重置所有 mock
vi.clearAllMocks();
});
// 建立 tests/day24/performanceHook.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
interface PerformanceMetrics {
testName: string;
startTime: number;
endTime?: number;
duration?: number;
}
const metrics: PerformanceMetrics[] = [];
describe('performance monitoring', () => {
beforeEach((test) => {
const metric: PerformanceMetrics = {
testName: test.task.name,
startTime: Date.now()
};
metrics.push(metric);
});
afterEach((test) => {
const metric = metrics.find(m => m.testName === test.task.name);
if (metric) {
metric.endTime = Date.now();
metric.duration = metric.endTime - metric.startTime;
// 記錄慢測試
if (metric.duration > 100) {
console.warn(`⚠️ 慢速測試: ${metric.testName} (${metric.duration}ms)`);
}
}
});
it('fast_test', () => {
expect(1 + 1).toBe(2);
});
it('slow_test', async () => {
await new Promise(resolve => setTimeout(resolve, 150));
expect(true).toBe(true);
});
});
試著建立快取管理 Hook,思考測試要點:清空測試快取、記錄快取使用、警告快取未命中、確保測試隔離。
今天我們學習了如何管理測試生命週期:
✅ 資料庫管理、API 認證、效能監控、資源清理
記住:好的生命週期管理讓測試更穩定可靠! 💪