iT邦幫忙

2025 iThome 鐵人賽

DAY 6
1

今天要做什麼?

昨天我們學會了測試生命週期,解決了測試污染的問題。但現在面對一個新的挑戰:「要測試同一個函數的多組輸入輸出,難道要寫幾十個類似的測試嗎?」

想像一個場景:你要為數學工具庫的 isPrime 函數寫測試,需要驗證很多數字。如果每組都寫一個獨立的測試,程式碼會變得非常冗長且難以維護。今天我們要學習「參數化測試」,用優雅的方式處理大量測試資料。

學習目標

今天結束後,你將學會:

  • 理解參數化測試的概念與價值
  • 掌握 it.each()test.each() 的使用
  • 學會設計有效的測試資料集
  • 理解資料驅動測試的最佳實踐

TDD 學習地圖

第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織
├── Day 05 - 測試生命週期
├── Day 06 - 參數化測試 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)

什麼是參數化測試? 📊

概念說明

參數化測試(Parameterized Testing)是一種測試技術,讓你能用同一組測試邏輯驗證多組不同的輸入資料。它有幾個別名:

  • 資料驅動測試(Data-Driven Testing)
  • 表格驅動測試(Table-Driven Testing)

傳統方式 vs 參數化測試

傳統方式需要為每個輸入寫一個獨立的測試,產生大量重複程式碼。參數化測試可以用一個測試函數處理多組資料:

// ✅ 參數化測試:乾淨、簡潔
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)
})

基本語法與用法 🔧

it.each() 的基本語法

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

測試資料設計最佳實踐 🎯

設計原則

設計測試資料時要考慮:

  1. 覆蓋重要情境:正常情況、邊界值、錯誤格式
  2. 分組相關資料:將類似的測試案例組織在一起
  3. 使用清楚的描述:讓測試失敗時容易理解問題

實戰練習:字串工具測試

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

避免常見陷阱 ⚠️

常見錯誤

  1. 測試資料過多:選擇代表性資料,避免執行時間過長
  2. 描述不清楚:使用清楚的測試描述,便於理解測試目的
  3. 缺乏分組:將相關測試資料分組,提高測試組織性

好與壞的參數化測試

// ❌ 錯誤:描述不清、邏輯混雜
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)
})

今天學到什麼?

今天我們深入學習了參數化測試的概念和實際應用:

核心概念

  • 參數化測試:用同一組邏輯測試多組資料
  • 資料驅動測試:讓測試資料決定測試行為
  • it.each() 語法:Vitest 的參數化測試實作

實用技巧

  • 陣列格式:簡單資料用陣列
  • 物件格式:複雜資料用物件
  • 描述性命名:讓測試結果易於理解

避免的陷阱

  • 資料過多:選擇代表性資料
  • 描述不清:使用清楚的測試描述
  • 缺乏分組:將相關測試資料分組

總結 🎆

參數化測試是提高測試效率和覆蓋率的強力工具:

  • 減少重複程式碼:一組邏輯測試多組資料
  • 提高測試覆蓋率:容易測試更多情境
  • 改善可維護性:集中管理測試資料
  • 增強可讀性:清楚的測試結構

記住:好的參數化測試資料設計是測試品質的關鍵。

明天我們將學習「測試替身基礎」,了解如何使用 Mock、Stub 等技術來隔離測試對象,讓測試更專注和可靠。


上一篇
Day 05 - 測試生命週期 🔄
下一篇
Day 07 - 測試替身基礎 🎭
系列文
React TDD 實戰:用 Vitest 打造可靠的前端應用9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言