iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0

重複地獄

你在 Code Review 時看到同事寫了 20 個幾乎一樣的密碼驗證測試,只有輸入值不同。今天你將學會參數化測試,讓一個測試勝過一百個重複的測試。

🗺️ 30 天旅程地圖

基礎概念 → 測試技能 → 實戰技巧 → 完整專案
          ★
    [Day6] 參數化測試

今天我們要學習參數化測試,用資料驅動的方式讓測試更有效率。

什麼是參數化測試?

參數化測試讓你:一次測試多種輸入資料驅動易於擴展更好維護

dataset() 基礎用法

建立 tests/day06/ParameterizedBasicsTest.php

<?php

// 定義資料集
dataset('valid_emails', [
    'simple' => 'user@example.com',
    'with_subdomain' => 'user@mail.example.com', 
    'with_plus' => 'user+test@example.com'
]);

dataset('invalid_emails', [
    'missing_at' => 'userexample.com',
    'missing_domain' => 'user@',
    'spaces' => 'user @example.com'
]);

test('validates email addresses correctly', function (string $email) {
    expect(filter_var($email, FILTER_VALIDATE_EMAIL))->not->toBeFalse();
})->with('valid_emails');

test('rejects invalid email addresses', function (string $email) {
    expect(filter_var($email, FILTER_VALIDATE_EMAIL))->toBeFalse();
})->with('invalid_emails');

內聯資料集

test('password strength validation', function (string $password, string $expectedStrength) {
    expect(getPasswordStrength($password))->toBe($expectedStrength);
})->with([
    ['123', 'weak'],
    ['password', 'weak'], 
    ['Password1', 'medium'],
    ['Password123!', 'strong']
]);

進階資料集應用

建立 tests/day06/ComplexDatasetTest.php

<?php

use App\PriceCalculator;

dataset('price_calculations', [
    'simple_item' => [
        'items' => [['price' => 100, 'quantity' => 2]],
        'discount' => 0,
        'tax_rate' => 0.05,
        'expected_total' => 210
    ],
    'with_discount' => [
        'items' => [['price' => 1000, 'quantity' => 1]],
        'discount' => 100,
        'tax_rate' => 0.05,
        'expected_total' => 945 // (1000-100) * 1.05
    ]
]);

test('calculates order total correctly', function (array $items, int $discount, float $taxRate, float $expectedTotal) {
    $calculator = new PriceCalculator();
    
    $total = $calculator->calculateTotal($items, $discount, $taxRate);
    
    expect($total)->toBe($expectedTotal);
})->with('price_calculations');

動態資料集

dataset('random_numbers', function () {
    for ($i = 1; $i <= 5; $i++) {
        yield "test_$i" => [rand(1, 100), rand(1, 100)];
    }
});

test('addition is commutative', function (int $a, int $b) {
    expect($a + $b)->toBe($b + $a);
})->with('random_numbers');

實戰案例:表單驗證系統

建立 tests/day06/FormValidationTest.php

<?php

use App\UserRegistrationForm;

dataset('valid_registration_data', [
    'basic_user' => [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'password' => 'SecurePassword123!',
        'age' => 25
    ]
]);

dataset('invalid_registration_data', [
    'empty_name' => [
        'data' => ['name' => '', 'email' => 'test@example.com', 'password' => 'Password123!', 'age' => 25],
        'expected_error' => 'Name is required'
    ],
    'weak_password' => [
        'data' => ['name' => 'John', 'email' => 'john@example.com', 'password' => '123', 'age' => 25],
        'expected_error' => 'Password too weak'
    ]
]);

test('accepts valid registration data', function (string $name, string $email, string $password, int $age) {
    $form = new UserRegistrationForm();
    
    $result = $form->validate([
        'name' => $name,
        'email' => $email,
        'password' => $password,
        'age' => $age
    ]);
    
    expect($result->isValid())->toBeTrue();
})->with('valid_registration_data');

test('rejects invalid registration data', function (array $data, string $expectedError) {
    $form = new UserRegistrationForm();
    
    $result = $form->validate($data);
    
    expect($result->isValid())->toBeFalse();
    expect($result->getErrors())->toContain($expectedError);
})->with('invalid_registration_data');

多維度測試

dataset('payment_methods', ['credit_card', 'paypal']);
dataset('currencies', ['TWD', 'USD']);

test('payment processing works for all combinations', function (string $paymentMethod, string $currency) {
    $processor = new PaymentProcessor();
    
    $result = $processor->process($paymentMethod, 1000, $currency);
    
    expect($result->isSuccessful())->toBeTrue();
    expect($result->getCurrency())->toBe($currency);
})->with('payment_methods')
  ->with('currencies');

// 這會產生 2 × 2 = 4 個測試案例

Pest 獨有的便捷語法

test('all users have valid email format')
    ->expect(User::all())
    ->each->email->toMatch('/^[^@]+@[^@]+\.[^@]+$/');

test('mathematical operations are consistent', function ($operation, $a, $b, $expected) {
    expect($operation($a, $b))->toBe($expected);
})->with([
    [fn($x, $y) => $x + $y, 2, 3, 5],
    [fn($x, $y) => $x * $y, 4, 5, 20]
]);

🎯 今日小挑戰

建立商品定價系統的參數化測試:

dataset('pricing_scenarios', [
    'regular_customer' => [
        'base_price' => 100,
        'quantity' => 5,
        'membership' => null,
        'expected_total' => 500
    ],
    'gold_member' => [
        'base_price' => 100,
        'quantity' => 10,
        'membership' => 'gold',
        'expected_total' => 850 // 15% discount
    ]
]);

test('pricing engine calculates correct totals', function (
    int $basePrice, 
    int $quantity, 
    ?string $membership, 
    int $expectedTotal
) {
    $engine = new PricingEngine();
    
    $total = $engine->calculateTotal($basePrice, $quantity, $membership);
    
    expect($total)->toBe($expectedTotal);
})->with('pricing_scenarios');

更多參數化測試範例

表單驗證測試

dataset('invalid_emails', [
    'no @ symbol' => ['testexample.com'],
    'no domain' => ['test@'],
    'no local part' => ['@example.com'],
    'double @' => ['test@@example.com'],
    'spaces' => ['test @example.com'],
]);

test('rejects invalid email formats', function ($email) {
    $validator = new EmailValidator();
    
    expect($validator->isValid($email))->toBeFalse();
})->with('invalid_emails');

日期處理測試

test('formats dates correctly', function ($input, $format, $expected) {
    $formatter = new DateFormatter();
    
    $result = $formatter->format($input, $format);
    
    expect($result)->toBe($expected);
})->with([
    'Y-m-d format' => ['2024-01-15', 'Y-m-d', '2024-01-15'],
    'd/m/Y format' => ['2024-01-15', 'd/m/Y', '15/01/2024'],
    'M j, Y format' => ['2024-01-15', 'M j, Y', 'Jan 15, 2024'],
    'timestamp' => ['2024-01-15', 'U', '1705276800'],
]);

今日重點回顧

核心概念

  • 參數化測試用資料集驅動測試執行
  • dataset() 定義測試資料,with() 應用資料集
  • 一個測試函數可以執行多次,每次使用不同參數

實用技巧

  • 使用有意義的資料集名稱和鍵值
  • 複雜資料用陣列結構組織
  • 多個資料集可以組合使用
  • 善用 Pest 的 each() 方法

明天預告

明天我們將進入測試替身基礎:Mock、Stub、Spy 的概念和差異,以及如何隔離外部依賴。

#iTHome鐵人賽 #LaravelTDD #Pest #PHP #測試驅動開發


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

尚未有邦友留言

立即登入留言