昨天我們學會了測試生命週期,解決了測試污染的問題。但現在面對一個新的挑戰:「要測試同一個函數的多組輸入輸出,難道要寫幾十個類似的測試嗎?」
想像一個場景:你要為數學工具庫的 isPrime
函數寫測試,需要驗證很多數字。如果每組都寫一個獨立的測試,程式碼會變得非常冗長且難以維護。今天我們要學習「參數化測試」,用優雅的方式處理大量測試資料。
今天結束後,你將學會:
it.each()
和 test.each()
的使用第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織
├── Day 05 - 測試生命週期
├── Day 06 - 參數化測試 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)
參數化測試(Parameterized Testing)是一種測試技術,讓你能用同一組測試邏輯驗證多組不同的輸入資料。它有幾個別名:
傳統方式需要為每個輸入寫一個獨立的測試,產生大量重複程式碼。參數化測試可以用一個測試函數處理多組資料:
// ✅ 參數化測試:乾淨、簡潔
it.each([
[2, true], [3, true], [5, true],
[4, false], [6, false],
])('checks if %i is prime, expects %s', (input, expected) => {
expect(isPrime(input)).toBe(expected)
})
Vitest 提供了 it.each()
和 test.each()
來實作參數化測試:
// 陣列格式
it.each([
[input1, expected1],
[input2, expected2],
[input3, expected3],
])('test description', (input, expected) => {
// 測試邏輯
})
// 物件格式
it.each([
{ input: 1, expected: true },
{ input: 2, expected: false },
])('test description', ({ input, expected }) => {
// 測試邏輯
})
建立 src/math/primeChecker.ts
:
export function isPrime(n: number): boolean {
if (n < 2) return false
if (n === 2) return true
if (n % 2 === 0) return false
for (let i = 3; i <= Math.sqrt(n); i += 2) {
if (n % i === 0) return false
}
return true
}
建立 tests/day06/prime-checker.test.ts
:
import { describe, it, expect } from 'vitest'
import { isPrime } from '../../src/math/primeChecker'
describe('prime checker', () => {
it.each([
[2, true], [3, true], [5, true], [7, true], [11, true],
[4, false], [6, false], [8, false], [9, false], [10, false],
])('checks if %i is prime, expects %s', (input, expected) => {
expect(isPrime(input)).toBe(expected)
})
it.each([
[0, false], [1, false], [-1, false], [-5, false],
])('edge case: checks if %i is prime, expects %s', (input, expected) => {
expect(isPrime(input)).toBe(expected)
})
it.each([
[97, true], [101, true], [997, true],
[100, false], [1000, false],
])('large number: checks if %i is prime, expects %s', (input, expected) => {
expect(isPrime(input)).toBe(expected)
})
})
當測試資料變複雜時,物件格式會更清楚:
import { describe, it, expect } from 'vitest'
describe('string validator', () => {
it.each([
{
input: 'hello@example.com',
expected: true,
description: '有效的電子郵件'
},
{
input: 'invalid-email',
expected: false,
description: '無效的電子郵件'
},
{
input: '@example.com',
expected: false,
description: '缺少用戶名'
},
{
input: 'user@',
expected: false,
description: '缺少域名'
}
])('$description: validates "$input", expects $expected',
({ input, expected }) => {
expect(isValidEmail(input)).toBe(expected)
}
)
})
建立 src/math/calculator.ts
:
export class Calculator {
add(a: number, b: number): number { return a + b }
subtract(a: number, b: number): number { return a - b }
multiply(a: number, b: number): number { return a * b }
divide(a: number, b: number): number {
if (b === 0) throw new Error('Division by zero')
return a / b
}
}
建立 tests/day06/calculator.test.ts
:
import { describe, it, expect, beforeEach } from 'vitest'
import { Calculator } from '../../src/math/calculator'
describe('calculator tests', () => {
let calculator: Calculator
beforeEach(() => { calculator = new Calculator() })
it.each([
[1, 2, 3],
[5, 3, 8],
[-1, 1, 0],
])('adds %i and %i to get %i', (a, b, expected) => {
expect(calculator.add(a, b)).toBe(expected)
})
it.each([
[5, 3, 2],
[10, 5, 5],
[0, 5, -5],
])('subtracts %i from %i to get %i', (a, b, expected) => {
expect(calculator.subtract(a, b)).toBe(expected)
})
it.each([
[5, 0],
[10, 0],
[-5, 0],
])('dividing %i by %i throws error', (a, b) => {
expect(() => calculator.divide(a, b)).toThrow('Division by zero')
})
})
設計測試資料時要考慮:
建立 src/utils/stringUtils.ts
:
export function capitalize(str: string): string {
if (!str) return ''
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
}
export function truncate(str: string, maxLength: number): string {
if (str.length <= maxLength) return str
return str.slice(0, maxLength - 3) + '...'
}
建立 tests/day06/string-utils.test.ts
:
import { describe, it, expect } from 'vitest'
import { capitalize, truncate } from '../../src/utils/stringUtils'
describe('string utilities', () => {
it.each([
['hello', 'Hello'], ['WORLD', 'World'],
['typescript', 'Typescript'], ['', ''],
])('capitalizes "%s" to "%s"', (input, expected) => {
expect(capitalize(input)).toBe(expected)
})
it.each([
{ str: 'hello world', max: 5, expected: 'he...' },
{ str: 'short', max: 10, expected: 'short' },
])('truncates "$str" to "$expected"', ({ str, max, expected }) => {
expect(truncate(str, max)).toBe(expected)
})
})
// ❌ 錯誤:描述不清、邏輯混雜
it.each([
['add', 1, 2, 3],
['subtract', 5, 2, 3],
])('test', (op, a, b, expected) => { /* 複雜邏輯 */ })
// ✅ 正確:清楚描述、單一職責
it.each([
[1, 2, 3],
[4, 5, 9],
])('adds %i and %i to equal %i', (a, b, expected) => {
expect(add(a, b)).toBe(expected)
})
今天我們深入學習了參數化測試的概念和實際應用:
參數化測試是提高測試效率和覆蓋率的強力工具:
記住:好的參數化測試資料設計是測試品質的關鍵。
明天我們將學習「測試替身基礎」,了解如何使用 Mock、Stub 等技術來隔離測試對象,讓測試更專注和可靠。