昨天我們學會了測試生命週期,解決了測試污染的問題。但現在面對一個新的挑戰:「要測試同一個函數的多組輸入輸出,難道要寫幾十個類似的測試嗎?」
想像一個場景:你要為數學工具庫的 isPrime
函數寫測試,需要驗證很多數字。如果每組都寫一個獨立的測試,程式碼會變得非常冗長且難以維護。今天我們要學習「參數化測試」,用優雅的方式處理大量測試資料。
今天結束後,你將學會:
with()
方法使用第一階段:打好基礎(Day 1-10)
├── Day 01 - 環境設置與第一個測試
├── Day 02 - 認識斷言(Assertions)
├── Day 03 - TDD 紅綠重構循環
├── Day 04 - 測試結構與組織
├── Day 05 - 測試生命週期
├── Day 06 - 參數化測試 ★ 今天在這裡
├── ...
└── (更多精彩內容待續)
參數化測試(Parameterized Testing)是一種測試技術,讓你能用同一組測試邏輯驗證多組不同的輸入資料。它有幾個別名:
傳統方式需要為每個輸入寫一個獨立的測試,產生大量重複程式碼。參數化測試可以用一個測試函數處理多組資料:
// ✅ 參數化測試:乾淨、簡潔
it('tests prime numbers', function($input, $expected) {
expect(isPrime($input))->toBe($expected);
})->with([
[2, true], [3, true], [5, true],
[4, false], [6, false],
]);
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],
]);
});
});
設計測試資料時要考慮:
建立 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'],
]);
// ❌ 錯誤:描述不清、邏輯混雜
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],
]);
今天我們深入學習了參數化測試的概念和實際應用:
參數化測試是提高測試效率和覆蓋率的強力工具:
記住:好的參數化測試資料設計是測試品質的關鍵。
明天我們將學習「測試替身基礎」,了解如何使用 Mock、Stub 等技術來隔離測試對象,讓測試更專注和可靠。