iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0

今天要做什麼?

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

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

學習目標

今天結束後,你將學會:

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

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('tests prime numbers', function($input, $expected) {
    expect(isPrime($input))->toBe($expected);
})->with([
    [2, true], [3, true], [5, true],
    [4, false], [6, false],
]);

基本語法與用法 🔧

with() 的基本語法

Pest 提供了 with() 方法來實作參數化測試:

// 基本語法
it('test description', function($param1, $param2) {
    // 測試邏輯
})->with([
    [$value1a, $value1b],
    [$value2a, $value2b],
    [$value3a, $value3b],
]);

// 關聯陣列格式
it('test description', function($input, $expected) {
    // 測試邏輯
})->with([
    'case 1' => [10, 100],
    'case 2' => [20, 400],
    'case 3' => [30, 900],
]);

實戰演練:建立質數檢測器

建立 app/Math/PrimeChecker.php

<?php

namespace App\Math;

class PrimeChecker
{
    public static function isPrime(int $n): bool
    {
        if ($n < 2) return false;
        if ($n === 2) return true;
        if ($n % 2 === 0) return false;
        
        for ($i = 3; $i <= sqrt($n); $i += 2) {
            if ($n % $i === 0) return false;
        }
        
        return true;
    }
}

建立 tests/Unit/Day06/PrimeCheckerTest.php

<?php

use App\Math\PrimeChecker;

describe('Prime Number Detector', function() {
    it('tests prime numbers', function($input, $expected) {
        expect(PrimeChecker::isPrime($input))->toBe($expected);
    })->with([
        [2, true],
        [3, true],
        [5, true],
        [7, true],
        [11, true],
        [4, false],
        [6, false],
        [8, false],
        [9, false],
        [10, false],
    ]);

    it('tests edge cases', function($input, $expected) {
        expect(PrimeChecker::isPrime($input))->toBe($expected);
    })->with([
        [0, false],
        [1, false],
        [-1, false],
        [-5, false],
    ]);

    it('tests large numbers', function($input, $expected) {
        expect(PrimeChecker::isPrime($input))->toBe($expected);
    })->with([
        [97, true],
        [101, true],
        [997, true],
        [100, false],
        [1000, false],
    ]);
});

具名測試資料

使用關聯陣列提高可讀性

當測試資料變複雜時,使用具名的關聯陣列會更清楚:

it('validates email addresses', function($email, $expected) {
    expect(isValidEmail($email))->toBe($expected);
})->with([
    'valid email' => ['user@example.com', true],
    'invalid email without @' => ['invalid-email', false],
    'invalid email without domain' => ['user@', false],
    'invalid email without user' => ['@example.com', false],
    'empty string' => ['', false],
]);

計算機測試範例 🧮

建立 app/Math/Calculator.php

<?php

namespace App\Math;

class Calculator
{
    public function add(int|float $a, int|float $b): int|float
    {
        return $a + $b;
    }

    public function subtract(int|float $a, int|float $b): int|float
    {
        return $a - $b;
    }

    public function multiply(int|float $a, int|float $b): int|float
    {
        return $a * $b;
    }

    public function divide(int|float $a, int|float $b): int|float
    {
        if ($b === 0) {
            throw new InvalidArgumentException('Division by zero');
        }
        return $a / $b;
    }
}

建立 tests/Unit/Day06/CalculatorTest.php

<?php

use App\Math\Calculator;

describe('Calculator Parameterized Tests', function() {
    beforeEach(function() {
        $this->calculator = new Calculator();
    });

    describe('Basic Operations Tests', function() {
        it('tests addition', function($a, $b, $expected) {
            expect($this->calculator->add($a, $b))->toBe($expected);
        })->with([
            [1, 2, 3],
            [5, 3, 8],
            [-1, 1, 0],
        ]);

        it('tests subtraction', function($a, $b, $expected) {
            expect($this->calculator->subtract($a, $b))->toBe($expected);
        })->with([
            [5, 3, 2],
            [10, 5, 5],
            [0, 5, -5],
        ]);
    });

    describe('Error Handling Tests', function() {
        it('throws exception for division by zero', function($a, $b) {
            expect(fn() => $this->calculator->divide($a, $b))
                ->toThrow(InvalidArgumentException::class, 'Division by zero');
        })->with([
            [5, 0],
            [10, 0],
            [-5, 0],
        ]);
    });
});

測試資料設計最佳實踐 🎯

設計原則

設計測試資料時要考慮:

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

實戰練習:字串工具測試

建立 app/Utils/StringUtils.php

<?php

namespace App\Utils;

class StringUtils
{
    public static function capitalize(string $str): string
    {
        if (empty($str)) return '';
        return ucfirst(strtolower($str));
    }

    public static function truncate(string $str, int $maxLength): string
    {
        if (strlen($str) <= $maxLength) return $str;
        return substr($str, 0, $maxLength - 3) . '...';
    }
}

建立 tests/Unit/Day06/StringUtilsTest.php

<?php

use App\Utils\StringUtils;

it('tests capitalize function', function($input, $expected) {
    expect(StringUtils::capitalize($input))->toBe($expected);
})->with([
    ['hello', 'Hello'],
    ['WORLD', 'World'],
    ['typescript', 'Typescript'],
    ['', ''],
]);

it('tests truncate function', function($str, $max, $expected) {
    expect(StringUtils::truncate($str, $max))->toBe($expected);
})->with([
    ['hello world', 5, 'he...'],
    ['short', 10, 'short'],
]);

避免常見陷阱 ⚠️

常見錯誤

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

好與壞的參數化測試

// ❌ 錯誤:描述不清、邏輯混雜
it('test', function($op, $a, $b, $expected) {
    // 複雜的條件邏輯
})->with([
    ['add', 1, 2, 3],
    ['subtract', 5, 2, 3],
]);

// ✅ 正確:清楚描述、單一職責
it('adds %i and %i to equal %i', function($a, $b, $expected) {
    expect(add($a, $b))->toBe($expected);
})->with([
    [1, 2, 3],
    [4, 5, 9],
]);

今天學到什麼?

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

核心概念

  • 參數化測試:用同一組邏輯測試多組資料
  • 資料驅動測試:讓測試資料決定測試行為
  • with() 方法:Pest 的參數化測試實作

實用技巧

  • 陣列格式:簡單資料用陣列
  • 關聯陣列:複雜資料用具名格式
  • 資料提供者:複雜資料用方法提供

避免的陷阱

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

總結 🎆

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

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

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

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


上一篇
Day 05 - 測試生命週期 🔄
下一篇
Day 07 - 測試替身基礎 🎭
系列文
Laravel Pest TDD 實戰:從零開始的測試驅動開發9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言