🎯 什麼情境該想到我
當你要改一段沒有測試的舊碼、想先建立「改壞就會被抓到」的安全網時。
第 13 章開頭就先把測試的用途分清楚(p.153):
「自動化測試是一個非常有用的工具,但這並非對尋找 bug 而言,至少並沒有直接的關係。一般而言,自動化測試的任務是明確說明一個我們想要實現的目標,或者試圖保持程式碼中某些既有的行為。在自然的開發流程中,屬於前者的測試逐漸就會變成後者。當然,你會遇到 bug,但通常並非在某個測試第一次運行的時候,而是在不小心改變了不想改變的行為時,這時運行測試就會指出問題。」
所以對遺留程式碼的做法是(p.153):
「最好就是把我們想要修改的那一塊區域先用測試給罩起來,像安全網那樣。」
書也擋掉了「翻需求文件來寫測試」這條路。那個辦法「的確是個辦法,但並不算很好」(p.153),理由是(p.154):
「因為對於幾乎所有的遺留系統而言,更重要的不是『系統應該能夠做些什麼』,而是『系統當前能夠做些什麼』。所以如果我們基於從文件當中發掘出來的關於『系統應該能夠做些什麼』的假設來編寫測試的話,就又回到了尋找 bug 的老路上了。尋找 bug 的確很重要,但我們當前的目標是把測試安置到位,從而減少程式碼修改過程當中的不確定性。」
⚙️ 怎麼用
定義:它記錄的是現狀,不是應然(p.154)
「我把用於行為保持的測試稱為特徵測試(Characterization test)。特徵測試刻畫了一塊程式碼的實際行為。而不是『嗯……這塊程式碼應該具有這一行為』或者『我想它會那樣的吧』。特徵測試描述了系統當前的實際行為。」
書對名字只給到這裡:這類測試的工作就是「刻畫」實際行為,13.2 節的標題也直接叫「刻畫類」。書中沒有再進一步解釋命名由來。
五個步驟(p.154,逐字)
「(1) 在測試用具中使用目標程式碼塊。
(2) 編寫一個你知道會失敗的斷言。
(3) 從斷言的失敗中得知程式碼的行為。
(4) 修改你的測試,讓它預期目標程式碼的實際行為。
(5) 重複上述步驟。」
書的示範(p.154–155):作者「相當確信」某個頁面產生器物件不會產生某個特定字串,於是寫下一個他知道會失敗的斷言;測試失敗的訊息告訴他那個新建物件實際回傳的是一個空串;把期望值改成空串,測試就通過了。
「現在測試通過了。而且,不僅是通過,它還起到了描述……一個最基本行為的作用」(p.155)
這樣做「不是在自欺欺人嗎?」——書自己提出並回答了這個質疑(p.155)
「比如,既然我們只是把程式碼產生的實際結果填入測試,那這些所謂的測試就沒任何意義了。要是程式碼本身就有 bug 的話,那我們填入測試的那些期望值豈不很可能都是錯的?
然而,如果我們換個角度的話,這個問題就消失了。我們不把它們看成軟體必須遵循的黃金準則,因為我們並不是為了尋找 bug,我們是想設置一個機制以便於以後尋找 bug。注意,這裡所說的 bug 是指後面可能出現的、與當前系統行為不一致的行為。」
採用這個視角之後,「它們不再是一個個的準則,而是描述了系統各部分的實際行為」;知道實際行為之後,「結合之前對於系統『應該具有的行為』的認識,就可以明智地作出如何修改的決策」(p.155)。書強調(p.155):
「我們通常可以通過與其他人交流或者通過計算知道哪些行為是需要添加的,但若是少了(特徵)測試的話,便沒法知道系統當前的實際行為是什麼了」
現狀行為看起來像 bug 時怎麼辦——書的立場很明確
第一層(p.155,方框):不是丟掉那個測試,是標記為可疑。
「特徵測試描述了一塊程式碼的實際行為。在編寫特徵測試的時候如果發現某些結果與我們所期望的不一致,最好弄清它。因為我們遇到的可能是個 bug。但這並不是說我們就不能把該測試放進測試套裝中,而是說我們應該將它標記為可疑的,並搞清修正它會帶來哪些影響。」
第二層(p.157,「發現 bug 時……」方框):預設是盡早修正,只有已部署時才先調查。
「答案要視具體情況而定。倘若系統尚未被部署,那答案很簡單:修正 bug。否則就需要調查調查,看看是否已經有用戶依賴於系統的當前行為(甚至在你看來是 bug 的行為)了。對於後一種情況,通常得花上一點時間來分析如何才能在不導致連鎖反應的前提下修正一個 bug。
我比較傾向於一旦發現 bug 就盡早修正它們。如果一個行為很明顯就是錯的,那就該進行修正。如果你覺得某個行為有問題,則可以把相應的測試程式碼標成可疑的,並逐漸提升其優先級。盡早查明它是否為 bug 以及最好如何處置它。」
同段也提醒(p.157):「只要是遺留程式碼就免不了有 bug,通常 bug 的量是跟它(遺留程式碼)被理解的程度成直接的比例關係的。」
該寫多少測試、什麼時候停(p.156)
對一個類到底可以寫多少特徵測試,「答案是,無窮多。花上十年八年也寫不完」。書給的判準:
「要想解答這個問題,關鍵的一點就是要意識到我們並不是在編寫黑盒測試。換句話說,我們在編寫特徵測試的時候是可以去查看所要刻畫的程式碼的。程式碼本身能夠告訴我們它們的行為,而如果看了程式碼還不能肯定的話,理想的辦法就是編寫測試去『詢問』它們了。」
兩步:
- 讓自己對目標程式碼的行為感到好奇——這是「編寫特徵測試的第一步」,「在這個階段我們不斷編寫測試直到感到已經理解了程式碼」。
- 設法弄清「我們的修改如果引入了 bug 的話測試能否『感應』得到」。「如果存在可能的漏網之魚,就要添加更多的測試,直到無遺漏為止。而如果我們沒有這麼大的信心,安全一點的辦法就是考慮換一種方式來修改程式碼。或許我們可以再返回到第一步看看。」
刻畫一個類:四個啟發式方法(13.2 節,p.156)
先「從較高的層面上來領會該類會做些什麼」,針對能想到的最簡單行為寫幾個測試,然後:
- 「尋找程式碼中邏輯複雜的部分。如果你不理解某塊程式碼,可以考慮引入感知變數(239 頁)來刻畫它。利用感知變數來確保程式碼中的某些特定的區域被執行到了。」
- 「隨著你不斷發現類或方法的一個個職責,不時停下來把你認為可能出錯的地方列一個單子。看看能不能編寫出能夠觸發這些問題的測試。」
- 「考慮你在測試中提供的輸入。如果故意把輸入的值變得極端化會出現什麼情況呢?」
- 「對於某個類的物件,有沒有某些條件在它的整個生命週期當中都是成立的?通常這被人們稱為不變式(invariant)。嘗試編寫測試去驗證它們。」——書補一句:「通常你可能需要重構才能夠發現這些不變式。但重構往往能夠給你帶來關於『程式碼應該怎樣』的新認識。」
書還提醒特徵測試是寫給人看的文件(p.156–157):換位思考,設想自己是「在面對一個以前從未見過的類」的讀者,想知道什麼、想以什麼順序知道;一開始放簡單的、說明目標類主要意圖的用例,然後放突出該類與眾不同之處的用例。
目標測試:確認測試真的蓋到你要改的那一段(13.3 節,p.157–161)
寫完理解性的測試之後,「接下來的工作就是看看我們的測試是否真的覆蓋了想要修改的地方」(p.157)。書的例子是把一個計算方法裡「頂層 if 語句提取到一個新的方法中,然後將這個新方法移到」另一個類別當中(p.158)。不會被改到的那個分支,「嚴格來說這個測試也不是必須的」;而「將會遭到修改」的那個 else 分支,才需要特地設計輸入去命中(p.158–159)。
兩個陷阱:
- 測試通過不代表分支被執行(p.159,方框):「在為程式碼分支編寫測試時,應該考慮除了那個分支被執行之外是否還存在其他能令測試通過的條件。如果不確定的話,可以使用一個感知變數或除錯器來確定你的測試是否恰好命中目標。」
- 型別轉換會把錯誤藏起來(p.159–160):把金額從整型改成雙精度型之後,真正的麻煩不是浮點誤差,「而是指除非我們小心選擇測試輸入資料,否則在提取方法的時候就可能會犯錯,而且永遠也不知道自己錯在哪」。書舉的錯法是提取出的方法「按整型而不是雙精度型來接受參數」——「在 Java 以及許多其他語言當中都允許從雙精度到整型的隱式轉換:運行時會把值截斷」,結果回傳值被截斷了測試卻依然通過,因為「如果我們僅僅是把整型賦給整型,那麼隱式轉換發生與否對結果是沒有影響的」(p.160)。「所以除非我們想辦法設計出能夠顯式導致這些錯誤的測試資料來,否則錯誤就被隱藏起來了。」(p.160)
四種對策(p.160–161):手動計算你期望得到的值,並在每一個存在型別轉換的地方留意是否可能截斷;用除錯器追蹤賦值運算,看某組輸入會導致什麼轉換;用感知變數確認特定路徑被覆蓋且目標轉換也被測到;或者乾脆刻畫一塊更小的程式碼(手頭有能安全提取方法的重構工具時,把大方法切開分別寫測試——但「並非所有的語言都有重構工具,而且有時候就算有也不一定會按照你設想的方式來提取方法」)。
「最有價值的特徵測試覆蓋某條特定的程式碼路徑並檢查這條路徑上的每個轉換。」(p.161,方框)
收尾:三條啟發式(13.4 節「編寫特徵測試的啟發式方法」,p.161,逐字)
「(1) 為準備修改的程式碼區域編寫測試,盡量編寫用例,直到覺得你已經理解了那塊程式碼的行為。
(2) 之後再開始考慮你所要進行的修改,並針對修改編寫測試。
(3) 如果想要提取或轉移某些功能,那就編寫測試來驗證這些行為的存在性和一致性,一種情況一種情況地編寫。確認你的測試覆蓋到了將被轉移的程式碼,確認這些程式碼被正確連接在系統中。最後別忘了測試型別轉換。」
🧪 我實際套用的紀錄
- 2026-07-14:(待填)
⚠️ 注意
- 它鎖的是「現況」(可能含 bug),但書並不主張放著不管:作者「比較傾向於一旦發現 bug 就盡早修正它們」,明顯錯的就修,不確定的標成可疑並逐漸提升優先級(p.157)。只有在系統已被部署時才先調查是否已有用戶依賴當前行為,再分析怎麼在不導致連鎖反應的前提下修正(p.157)。
- 手動測試撐不住(p.153):「在我所接觸過的團隊中,幾乎每一個依賴於手動測試的團隊最終都遠遠落在了後面,結果團隊的信心大受打擊。」
- 不要把「找出並修正所有 bug」當目標(p.153):「對於大多數遺留程式碼來說,若是我們將尋找和修正所有 bug 當成目標的話,則永遠也不會有做完的一天。」書給的策略是「關心如何才能從一開始就避免讓 bug 進入程式碼」。
- 重構時要關心兩件事(p.160,方框):「一是目標行為在重構之後是否仍然存在,二是它是否正確『連接』在系統當中。許多特徵測試就像定心丸一樣。它們不去測試大量的特殊情況,而只是檢驗某些特定的行為是否存在。在一番移動或提取程式碼的重構之後,只要看到這些行為仍然存在,我們就可以放心地告訴自己:我們的重構保持了行為。」
- 使用方法的規則(p.156,方框):「當準備在遺留系統中使用一個方法之前,請查看一下是否已有針對它的測試。沒有的話就自己寫一個。始終保持這一習慣,你的測試就能起到資訊傳遞媒介的作用。」
🔗 相關工具
- 工具-遺留程式碼修改流程 —— 特徵測試是該流程第 4 步「編寫測試」要寫的東西
- 工具-接縫 —— 前一步,先找到能插進去的位置,才有辦法把現狀行為錄下來
- 工具-打破依賴以便測試 —— 步驟 (1)「在測試用具中使用目標程式碼塊」卡住時,要解的就是依賴問題
- 工具-影響分析 —— 13.3「目標測試」在問的正是「測試有沒有蓋到我要改的那一段」
- 工具-理解陌生程式碼 —— 同一件事的兩面:書說寫特徵測試的第一步是「讓自己對目標程式碼的行為感到好奇」,測試本身就是拿來「詢問」程式碼的(p.156)
- 工具-小步重構 —— 後一步,安全網架好之後才開始真正動結構
- 回到來源:修改程式碼的藝術