「TDD 教的不是應該先寫測試、再寫功能嗎?昨天那筆 commit 看起來完全不是這樣,這樣算不算沒紀律?」
先說結論:昨天那筆 commit 裡,確實有一部分是「先寫測試、還是先寫功能」都無所謂的教科書節奏,但也有一部分是完全不同的節奏——趁著改一個檔案的機會,把同一個檔案裡早就存在、卻一直沒被測到的舊功能,一併補上測試。今天把這兩種節奏拆開來看,講清楚為什麼後者不是「沒紀律」,而是真實開發現場常見、卻很少被拿出來討論的模式。
80d3469,具體指出哪部分屬於哪種節奏這是最常被教的節奏,昨天那筆 commit 裡也確實有這部分:getBNPLFields() 是全新寫的程式碼,testBNPLGetData() 是專門為它寫的新測試。功能跟測試一起誕生,誰先誰後不重要,重要的是兩者的範圍完全對得上——這個功能被寫了多少,就有多少測試覆蓋它。
testATMGetData()、testFlexibleInstallmentGetData() 這兩個測試,驗證的是早就存在的 getATMFields() 邏輯跟彈性分期欄位設定——這些程式碼在這次 commit 之前就能動、就在生產環境裡被使用者用著,只是完全沒有測試保護。
會出現這種節奏,通常不是刻意規劃出來的,而是一個很實際的心理過程:開發者為了驗證新加的 BNPL 邏輯,勢必要打開 PurchaseRequestTest.php、看懂裡面既有的測試怎麼組資料。看的過程中,很容易順手注意到「欸,ATM 跟彈性分期怎麼都沒有對應的測試」,然後趁著人已經在這個檔案裡、上下文都還在腦中的時候,一併把它們補上。
這不是照表操課的紀律,是一種「反正都打開了,不補可惜」的機會成本判斷。
❌ 把 commit message 照字面當成單一敘事
"這次加了 BNPL,也補了 ATM/BNPL/彈性分期的測試"
→ 讀起來像是三種付款方式都是這次一起完成的新工作,
容易誤判「這個套件的測試很跟得上開發進度」
✅ 拆開兩種節奏理解
"這次真正的新功能只有 BNPL,測試也照節奏配上了;
ATM 跟彈性分期的測試,是「趁著改這個檔案的機會」
補回過去欠下的測試債"
→ 更準確地反映這個套件測試覆蓋的真實累積方式:
不是規劃出來的,是一次次「順路」疊加出來的
如果只把這件事解讀成「這個套件測試補得不夠即時」,就錯過了更有用的觀察:「順路補測試」證明了『改動觸發』是測試債務被償還的一種真實機制,即使沒有制度化的覆蓋率門檻在推動。開發者不需要被規則強迫,光是因為要碰同一個檔案,自然而然就會多看一眼旁邊缺了什麼。
但這個機制有個明顯的弱點:它完全依賴「有沒有人剛好去動那個檔案」。如果 RefundRequest、VoidRequest 一直沒有新功能需求、沒有人因為別的理由打開那兩個檔案,它們現有的測試缺口就會一直停在那裡,沒有「順路」的機會可以觸發補救——這正是明天要看的對照組。
回想你自己補測試的經驗,有多少次是「規劃好要補」,又有多少次是「因為要改別的東西,順便看到才補」?如果後者佔比更高,這代表什麼——你的測試覆蓋率的成長,主要是被什麼在推動?
80d3469 裡 BNPL 屬於前者,ATM/彈性分期的測試屬於後者明天看這個機制的反面案例:RefundRequest、VoidRequest 至今還沒等到那個「順路」的機會,測試還停在最初的樣子。