當你嘗試為既有程式碼撰寫測試時,幾乎都會發現程式碼天生不適合測試。程式語言本身似乎就不支援可測試性。要得到容易測試的程式,唯一的辦法是在開發過程中就考慮測試,或刻意「為可測試性而設計」。
從「一大張文字」到「接縫」#
早期的程式設計經驗中,程式就是一長串指令的列表——一張巨大的文字。但當你開始為了測試而拆解程式碼時,會發現需要用截然不同的角度看待程式:不再是一張靜態的文字,而是一個充滿可替換點的結構。
什麼是接縫(Seam)#
接縫(Seam) 是程式中一個可以改變行為、但不需要修改該處原始碼的地方。
這是本書最核心的概念之一。當你找到一個接縫,就找到了一個可以在測試中替換行為的機會。
接縫與啟用點(Enabling Point)#
每個接縫都有一個對應的啟用點(Enabling Point)——你在那裡做出決定,選擇使用哪一種行為。
接縫讓我們看到程式碼中已經存在的機會:如果我們可以在接縫處替換行為,就能在測試中選擇性地排除依賴、感測條件、撰寫測試。
接縫的類型#
將程式文字轉化為可執行程式的每個步驟,都會暴露不同類型的接縫。
預處理接縫(Preprocessing Seams)#
在 C 和 C++ 中,巨集預處理器在編譯器之前執行,提供了文字替換的接縫。
例如,程式碼中呼叫了 db_update 這個難以測試的函式庫。透過引入一個 localdefs.h 標頭檔,可以在測試時用巨集替換 db_update 的呼叫:
#ifdef TESTING
// ...
#define db_update(account_no, item) \
{last_item = (item); last_account_no = (account_no);}
// ...
#endif- 接縫:
db_update的呼叫點 - 啟用點:
TESTING預處理定義
不建議在正式程式碼中大量使用預處理器,因為條件編譯指令會降低程式碼的可讀性,巨集也容易隱藏難以察覺的 Bug。但作為打破測試依賴的手段,它是 C/C++ 中額外的武器。
連結接縫(Link Seams)#
在許多語言中,編譯不是建構流程的最後一步。編譯器產生中間表示,連結器(Linker)將不同檔案的呼叫解析為完整程式。
- 在 C/C++ 中,有獨立的連結器
- 在 Java 中,編譯器透過 classpath 環境變數尋找類別
啟用點是 classpath 或建構腳本中的設定。透過調整 classpath,可以替換整個類別來進行測試。
使用連結接縫時,務必確保替代的程式碼與正式環境的差異是清晰可見的。如果分散在整個建構流程中,除錯時會非常困難。
物件接縫(Object Seams)#
物件接縫是最常用也最有用的接縫類型。在物件導向語言中,可以透過:
- 在類別中新增虛擬方法(virtual method),然後在子類別中覆寫
- 引入介面,透過多型替換行為

Figure 4.1: Cell hierarchy
以 C++ 為例,CAsyncSslRec::Init() 方法中呼叫了全域函式 PostReceiveError,這會造成討厭的副作用。解決方法:
- 在
CAsyncSslRec類別中新增同名的虛擬方法,委託給全域函式 - 建立子類別
TestingAsyncSslRec,覆寫該方法為空實作 - 在測試中使用子類別,消除副作用
class TestingAsyncSslRec : public CAsyncSslRec
{
virtual void PostReceiveError(UINT type, UINT errorcode)
{
}
};- 接縫:
PostReceiveError的呼叫點 - 啟用點:建立物件的地方(選擇使用
CAsyncSslRec或TestingAsyncSslRec)
為什麼接縫模型很重要#
接縫的思維幫助我們看到程式碼中已經存在的可替換機會。面對 legacy code 時,關鍵不是重寫,而是找到接縫、利用接縫。當你開始用接縫的角度看待程式碼,就能找到方法在不大幅改動的前提下將程式碼納入測試。