你在 Code Review 時看到同事寫了 20 個幾乎一樣的密碼驗證測試,只有輸入值不同。今天你將學會參數化測試,讓一個測試勝過一百個重複的測試。
基礎概念 → 測試技能 → 實戰技巧 → 完整專案
★
[Day6] 參數化測試
今天我們要學習參數化測試,用資料驅動的方式讓測試更有效率。
參數化測試讓你:一次測試多種輸入、資料驅動、易於擴展、更好維護
建立 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 個測試案例
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()
應用資料集實用技巧:
each()
方法明天我們將進入測試替身基礎:Mock、Stub、Spy 的概念和差異,以及如何隔離外部依賴。
#iTHome鐵人賽 #LaravelTDD #Pest #PHP #測試驅動開發