單元測試中,針對多參數函數的全組合測試往往面臨「參數爆炸」問題。本文實現一個輕量級 PHP 測試框架,自動生成參數笛卡爾積組合,執行目標函數並格式化輸出結果,支援陣列、布爾、標量等多類型參數,適合快速驗證純邏輯函數。
輸入:二維陣列 `['param_name' => [值1, 值2, ...]]`
輸出:所有參數組合的笛卡爾積陣列
<?php
/**
* 參數組合生成器:計算 C(n1,1) * C(n2,1) * ... * C(nk,1)
* @param array $combinList 二維陣列 [參數名 => [可選值...]]
* @return array 組合陣列 [組合索引 => [參數名 => 值]]
*/
function sysCombineArray(array $combinList): array {
// 驗證輸入
foreach ($combinList as $values) {
if (!is_array($values)) {
throw new InvalidArgumentException('參數值必須為陣列');
}
}
// 計算總組合數
$total = 1;
foreach ($combinList as $values) {
$total *= count($values);
}
$result = [];
$repeat = $total;
// 按參數順序生成組合
foreach ($combinList as $paramName => $values) {
$repeat /= count($values); // 當前值重複次數
$space = $total / count($values) / $repeat; // 值切換間隔
$start = 1;
foreach ($values as $value) {
$pos = $start;
for ($s = 0; $s < $space; $s++) {
for ($r = 0; $r < $repeat; $r++) {
$result[$pos][$paramName] = $value;
$pos++;
}
$pos += $repeat * (count($values) - 1);
}
$start += $repeat;
}
}
// 重建索引 (0-based)
return array_values($result);
}
// 測試組合生成
$params = [
'color' => ['red', 'green', 'blue'],
'size' => ['S', 'M', 'L'],
'qty' => [1, 2],
];
$combinations = sysCombineArray($params);
echo "總組合數: " . count($combinations) . " (預期 3*3*2=18)\n";
print_r($combinations);
<?php
/**
* 函數自動化測試
* @param string $funcName 目標函數名
* @param array $paramList 參數組合列表
* @return array 測試結果記錄
*/
function sysTestFunction(string $funcName, array $paramList): array {
if (!function_exists($funcName)) {
throw new BadFunctionCallException("函數 $funcName 不存在");
}
$combinations = sysCombineArray($paramList);
$results = [];
echo "開始測試函數: $funcName (共 " . count($combinations) . " 組參數)\n";
foreach ($combinations as $idx => $args) {
echo "\n--- 組合 #$idx ---\n";
// 顯示參數詳情
foreach ($args as $k => $v) {
$type = gettype($v);
$display = $type === 'array' ? 'Array(' . count($v) . ')' :
($type === 'boolean' ? ($v ? 'true' : 'false') :
($type === 'NULL' ? 'null' : $v));
echo " $k ($type) = $display\n";
}
// 構建調用參數
$callArgs = [];
foreach ($args as $k => $v) {
$callArgs[] = var_export($v, true);
}
$callStr = "$funcName(" . implode(', ', $callArgs) . ")";
// 執行並捕獲結果/異常
$start = microtime(true);
try {
$return = eval("return $callStr;");
$time = microtime(true) - $start;
$retType = gettype($return);
$retDisplay = $retType === 'array' ? 'Array(' . count($return) . ')' :
($retType === 'boolean' ? ($return ? 'true' : 'false') :
($retType === 'NULL' ? 'null' : $return));
echo " 執行: $callStr\n";
echo " 返回 ($retType): $retDisplay\n";
echo " 耗時: " . round($time * 1000, 3) . " ms\n";
$results[] = [
'index' => $idx,
'args' => $args,
'return' => $return,
'success' => true,
'time_ms' => round($time * 1000, 3),
];
} catch (Throwable $e) {
$time = microtime(true) - $start;
echo " 異常: {$e->getMessage()}\n";
echo " 耗時: " . round($time * 1000, 3) . " ms\n";
$results[] = [
'index' => $idx,
'args' => $args,
'error' => $e->getMessage(),
'success' => false,
'time_ms' => round($time * 1000, 3),
];
}
}
// 統計摘要
$passed = array_filter($results, fn($r) => $r['success']);
$failed = array_filter($results, fn($r) => !$r['success']);
echo "\n=== 測試摘要 ===\n";
echo "總計: " . count($results) . "\n";
echo "通過: " . count($passed) . "\n";
echo "失敗: " . count($failed) . "\n";
return $results;
}
// ==================== 使用示例 ====================
// 待測函數
function calculatePrice(float $base, string $grade, bool $vip, array $coupons = []): float {
$multiplier = match($grade) {
'A' => 1.0, 'B' => 1.1, 'C' => 1.2, default => 1.3
};
$price = $base * $multiplier;
if ($vip) $price *= 0.9;
foreach ($coupons as $c) {
if (isset($c['type'], $c['value'])) {
$price = $c['type'] === 'percent' ? $price * (1 - $c['value']) : $price - $c['value'];
}
}
return max(0, round($price, 2));
}
// 定義測試參數空間
$testParams = [
'base' => [100, 200, 500],
'grade' => ['A', 'B', 'C', 'D'],
'vip' => [true, false],
'coupons' => [
[],
[['type' => 'percent', 'value' => 0.1]],
[['type' => 'fixed', 'value' => 20]],
[['type' => 'percent', 'value' => 0.05], ['type' => 'fixed', 'value' => 10]],
],
];
// 執行測試
$results = sysTestFunction('calculatePrice', $testParams);
// 導出 CSV
$fp = fopen('test_results.csv', 'w');
fputcsv($fp, ['Index', 'Args', 'Return', 'Success', 'Time_ms']);
foreach ($results as $r) {
fputcsv($fp, [
$r['index'],
json_encode($r['args'], JSON_UNESCAPED_UNICODE),
$r['success'] ? var_export($r['return'], true) : '',
$r['success'] ? 'PASS' : 'FAIL',
$r['time_ms'],
]);
}
fclose($fp);
echo "\n結果已導出 test_results.csv\n";
| 功能 | 實現思路 |
|---|---|
| 成對測試 | 引入 Orthogonal Array / All-pairs 算法,大幅減少組合數 |
| 屬性測試 | 類似 QuickCheck,隨機生成滿足約束的輸入驗證不變量 |
| 並行執行 | 使用 pcntl_fork 或進程池並行運行獨立測試用例 |
| 代碼覆蓋率 | 整合 Xdebug/Xhprof 收集測試覆蓋的代碼路徑 |
| 數據驅動測試 | 從 CSV/JSON/數據庫讀取測試用例,支援參數化 |
| 斷言庫 | 引入 PHPUnit 斷言或自實現 assertEquals/assertTrue 等 |
自動化參數組合測試能快速發現邊界條件下的邏輯錯誤。本框架雖簡陋,但展示了「參數空間定義 → 笛卡爾積生成 → 執行驗證 → 結果收集」的完整鏈路。生產環境建議遷移至 PHPUnit + 數據提供者 或 Pest 框架,獲得更豐富的斷言、報告、並行、覆蓋率等企業級能力。