iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Modern Web

React TDD 實戰:用 Vitest 打造可靠的前端應用系列 第 24

Day 24 - 測試生命週期 Hook 🔄

  • 分享至 

  • xImage
  •  

你有沒有遇過這種情況? 🤔

「為什麼我的測試總是跑不過?明明程式碼都沒問題啊!」經過一番調查,發現是因為他的測試環境設置不一致。這讓我想到,如果能在測試的關鍵時刻自動執行一些設置或清理工作,是不是就能避免這種問題了?

今天我們要學習 Vitest 的測試生命週期 Hook 功能,確保測試環境的一致性!

本日學習地圖 🗺️

基礎篇          Kata 篇           框架篇
Day 1-10        Day 11-17        Day 18-24 ← 我們在這裡!
[=====]         [=====]          [=====>]

測試生命週期管理
├── 🔄 beforeEach/afterEach
├── 📦 beforeAll/afterAll  
├── ⚡ 測試環境設置
└── 🎯 測試資源管理

為什麼需要測試生命週期 Hook? 🎯

測試生命週期 Hook 是在測試執行的特定時機點自動觸發的程式碼。它們幫助我們管理測試環境,確保每個測試都在相同的條件下執行。

測試挑戰

  • 環境不一致 - 測試間相互影響
  • 重複設置 - 每個測試都要寫相同程式碼
  • 資源清理 - 忘記清理導致測試失敗

使用生命週期 Hook 📦

Vitest 提供了生命週期 Hook 來管理測試環境。

建立 tests/day24/lifecycle.test.ts

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 來管理測試資料。

建立 tests/day24/databaseHook.test.ts

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);
  });
});

測試條件式設置 ⚡

建立 tests/day24/conditionalHook.test.ts

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');
  });
});

測試 API 認證設置 🔁

// 建立 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');
  });
});

全域生命週期設定 🌍

建立 tests/setup.ts

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,思考測試要點:清空測試快取、記錄快取使用、警告快取未命中、確保測試隔離。

本日重點回顧 📝

今天我們學習了如何管理測試生命週期:

核心概念

  • beforeEach/afterEach - 每個測試的設置與清理
  • beforeAll/afterAll - 測試套件的設置與清理
  • 條件式設置 - 根據測試需求動態設置
  • 全域設定 - 跨測試檔案共享設置

測試技巧

✅ 資料庫管理、API 認證、效能監控、資源清理

延伸思考 💭

  • 如何設計通用的測試設置?
  • 如何避免 Hook 之間的依賴?
  • 如何確保 Hook 的執行效能?

記住:好的生命週期管理讓測試更穩定可靠! 💪


上一篇
Day 23 - 測試篩選與路由 🔀
下一篇
Day 25 - 整合測試 🔗
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言