測試綠燈只代表「斷言的部分都成立」,不代表「這段邏輯所有重要的行為都被驗證過」。如果斷言本身斷得不夠精準,一段邏輯即使被改壞了,測試依然會綠燈——這種假陽性測試比完全沒有測試更危險,因為它給了一種「有測試保護」的錯覺。
以 Day 20 講過的「合併前驗證片段數量」邏輯為例:
❌ 反例:斷言只確認「沒有拋例外」
async def test_merge_with_complete_segments():
await process(segments=make_segments(3), directory='video/item')
# 測試跑完沒有拋例外,就當作「驗證通過」
這個測試只驗證了「正常情況下不會出錯」,但完全沒有驗證「數量不吻合時真的會被攔下來」——如果 Day 20 的驗證邏輯被不小心刪掉或改壞,這個測試依然會綠燈,因為它從來沒有測過「應該失敗」的情況。
✅ 正例:明確斷言「該失敗的情況真的會失敗」
async def test_merge_raises_when_segments_incomplete():
with pytest.raises(IncompleteDownloadError):
await process(segments=make_segments(3), directory='video/incomplete-item')
# 明確驗證:片段不完整時,一定會拋出這個例外,不會被悄悄放行
一個實用的檢查方法:故意把被測試的邏輯改壞(例如刪掉驗證那一行),看看測試會不會失敗。如果測試依然綠燈,代表這個測試沒有真的驗證到那段邏輯的行為,只是「跑過去了」而已。這個技巧有個正式名稱叫「突變測試」(mutation testing),概念上就是刻意注入錯誤,反過來檢驗測試本身有沒有能力抓到這個錯誤。
回顧這個模式的根源,通常是測試只涵蓋了「正常情況下程式能跑完」,卻沒有反過來驗證「異常情況下程式真的會如預期般失敗或攔下問題」。一段防禦性邏輯(驗證、例外拋出、fail fast)如果沒有對應的測試去驗證「它真的會在該發生作用的時候發生作用」,這段防禦性邏輯本身其實跟不存在沒有太大差別。
挑一段你寫過的防禦性邏輯(驗證、例外處理),試著故意把它改壞,看看對應的測試會不會真的失敗。如果不會,這個測試斷言到了什麼?
明天講測試替身選錯層的問題:mock 太底層,改一次實作就要改一次測試。