「AI 幫這個方法寫了三個測試,全部綠燈,這樣應該可以放心合併了吧?」
這句話聽起來很合理——測試通過本來就是我們判斷「這段程式碼沒問題」的依據。但今天要講一個更基本的問題:測試通過,只代表這段測試程式碼裡寫的斷言都成立,不代表這段測試真的驗證了你在意的那件事。 如果斷言本身沒有對準任何有意義的行為,測試永遠是綠燈——不是因為程式碼正確,而是因為測試根本沒有能力讓它變紅。
假設有一個訂單服務,其中一個方法負責套用折扣:
class OrderService
{
public function applyDiscount(Order $order, float $rate): Order
{
$order->total = $order->total * (1 - $rate);
return $order;
}
}
請 AI 幫這個方法補測試,得到的結果可能長這樣:
❌ 全部綠燈,但沒測到任何有意義的行為:
public function test_apply_discount_returns_order()
{
$service = new OrderService();
$order = new Order();
$order->total = 100;
$result = $service->applyDiscount($order, 0.1);
$this->assertNotNull($result);
}
public function test_apply_discount_does_not_throw()
{
$service = new OrderService();
$order = new Order();
$order->total = 100;
$service->applyDiscount($order, 0.1);
$this->assertTrue(true);
}
public function test_apply_discount_returns_order_instance()
{
$service = new OrderService();
$order = new Order();
$result = $service->applyDiscount($order, 0.1);
$this->assertInstanceOf(Order::class, $result);
}
三個測試,三個綠燈。但仔細看斷言在檢查什麼:第一個只確認回傳值不是 null;第二個只確認呼叫過程沒有拋出例外;第三個只確認回傳的型別是 Order。沒有一個斷言碰到「折扣到底有沒有正確套用」這件事。 如果把 applyDiscount 的實作改成直接回傳原封不動的 $order(完全沒套用折扣),這三個測試依然全部通過。
這正是這系列的主題句在測試品質上的具體樣貌:AI 可以很快地產生語法正確、結構完整的測試,但「這個測試值不值得留」是品質判斷,不是語法判斷,AI 不會自動幫你做這個判斷。
會出現這種測試,通常不是 AI 偷懶,而是它拿到的資訊只有方法簽章跟型別:applyDiscount(Order $order, float $rate): Order。從簽章本身,AI 可以合理推論「這個方法應該回傳一個 Order、不應該拋出例外」,於是就針對這些「型別層級的保證」寫斷言——這些斷言不算錯,只是遠遠不夠。真正該驗證的業務規則(折扣有沒有正確套用到金額上),沒有寫在方法簽章裡,只存在於實作的邏輯本身,AI 如果沒有被明確要求,不會主動去猜這件事重不重要。
用一組對照來看差異:
✅ 對準業務規則的測試:
public function test_apply_discount_reduces_total_by_rate()
{
$service = new OrderService();
$order = new Order();
$order->total = 100;
$result = $service->applyDiscount($order, 0.1);
$this->assertEquals(90, $result->total);
}
這個版本只有一個斷言,但它真正驗證了業務規則本身:輸入 100 元、10% 折扣,期望結果是 90 元。如果實作被改壞(例如折扣公式寫反、或乾脆不套用折扣),這個斷言會立刻變紅。前面那三個「全綠燈」的版本,不管實作對不對,永遠不會變紅——它們不是在保護程式碼,只是在製造一種「有寫測試」的錯覺。
判斷一份測試是不是有效,最直接的方法不是讀斷言寫了什麼,而是動手把實作改壞一次,看測試會不會變紅。 如果你故意把 applyDiscount 的邏輯改成錯的(例如把 1 - $rate 改成 1 + $rate),跑一次測試——如果測試還是全部綠燈,代表這份測試對這個 bug 完全沒有防護力,不管它看起來寫了多少個案例。
這個方法之所以重要,是因為它把「這份測試有沒有意義」變成一個可以被驗證的具體問題,而不是憑感覺判斷「這看起來測試寫得蠻完整的」。這也是接下來這個系列會反覆用到的判斷工具:AI 給出的「已經寫好測試」這句話,跟 Day 01 講過的「已確認」結論是同一種東西——查證範圍不夠,結論就不可信。
回想你手上最近一段由 AI 補上的測試:如果你故意把對應的實作邏輯改壞一行,這份測試會變紅嗎?如果你不確定答案,那你其實還沒真正驗證過這份測試的價值。
明天要把今天看到的問題系統化:假陽性測試常見的三種樣貌——空斷言、恆真斷言、Mock 掉待測物件本身,各自的辨識方式跟修正方向。