iT邦幫忙

2026 iThome 鐵人賽

DAY 4
1
Software Development

AI 時代的 TDD:讓 AI 寫 Code,但不要讓它決定品質系列 第 4

Day 04:案例——AI 生成的測試全綠,卻沒測到任何有意義的行為

  • 分享至 

  • xImage
  •  

前言:測試都通過了,為什麼還要懷疑?

「AI 幫這個方法寫了三個測試,全部綠燈,這樣應該可以放心合併了吧?」

這句話聽起來很合理——測試通過本來就是我們判斷「這段程式碼沒問題」的依據。但今天要講一個更基本的問題:測試通過,只代表這段測試程式碼裡寫的斷言都成立,不代表這段測試真的驗證了你在意的那件事。 如果斷言本身沒有對準任何有意義的行為,測試永遠是綠燈——不是因為程式碼正確,而是因為測試根本沒有能力讓它變紅。

今日目標

  • 看一個具體案例: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 不會自動幫你做這個判斷。

會出現這種測試,通常不是 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 補上的測試:如果你故意把對應的實作邏輯改壞一行,這份測試會變紅嗎?如果你不確定答案,那你其實還沒真正驗證過這份測試的價值。

今日重點回顧

  • 測試通過只代表斷言本身成立,不代表斷言對準了你在意的業務行為
  • AI 容易對著方法簽章(型別、有沒有拋例外)寫測試,而不是對著業務規則寫測試,因為業務規則只存在於實作邏輯裡,沒有明確要求不會主動去驗證
  • 判斷測試是否有效的簡單方法:故意把實作改壞,看測試會不會變紅
  • 「已經寫好測試」這句話一樣需要查證範圍夠不夠,不能照單全收

明日預告

明天要把今天看到的問題系統化:假陽性測試常見的三種樣貌——空斷言、恆真斷言、Mock 掉待測物件本身,各自的辨識方式跟修正方向。


上一篇
Day 03:AI 讓「寫測試」變快,但「寫對測試」的門檻沒有變低
系列文
AI 時代的 TDD:讓 AI 寫 Code,但不要讓它決定品質4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言