當你嘗試為既有程式碼撰寫測試時,幾乎都會發現程式碼天生不適合測試。程式語言本身似乎就不支援可測試性。要得到容易測試的程式,唯一的辦法是在開發過程中就考慮測試,或刻意「為可測試性而設計」。

從「一大張文字」到「接縫」#

早期的程式設計經驗中,程式就是一長串指令的列表——一張巨大的文字。但當你開始為了測試而拆解程式碼時,會發現需要用截然不同的角度看待程式:不再是一張靜態的文字,而是一個充滿可替換點的結構。

什麼是接縫(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++ 中額外的武器。

在許多語言中,編譯不是建構流程的最後一步。編譯器產生中間表示,連結器(Linker)將不同檔案的呼叫解析為完整程式。

  • 在 C/C++ 中,有獨立的連結器
  • 在 Java 中,編譯器透過 classpath 環境變數尋找類別

啟用點是 classpath 或建構腳本中的設定。透過調整 classpath,可以替換整個類別來進行測試。

使用連結接縫時,務必確保替代的程式碼與正式環境的差異是清晰可見的。如果分散在整個建構流程中,除錯時會非常困難。

物件接縫(Object Seams)#

物件接縫是最常用也最有用的接縫類型。在物件導向語言中,可以透過:

  • 在類別中新增虛擬方法(virtual method),然後在子類別中覆寫
  • 引入介面,透過多型替換行為

Figure 4.1: Cell hierarchy

以 C++ 為例,CAsyncSslRec::Init() 方法中呼叫了全域函式 PostReceiveError,這會造成討厭的副作用。解決方法:

  1. CAsyncSslRec 類別中新增同名的虛擬方法,委託給全域函式
  2. 建立子類別 TestingAsyncSslRec,覆寫該方法為空實作
  3. 在測試中使用子類別,消除副作用
class TestingAsyncSslRec : public CAsyncSslRec
{
    virtual void PostReceiveError(UINT type, UINT errorcode)
    {
    }
};
  • 接縫PostReceiveError 的呼叫點
  • 啟用點:建立物件的地方(選擇使用 CAsyncSslRecTestingAsyncSslRec

為什麼接縫模型很重要#

接縫的思維幫助我們看到程式碼中已經存在的可替換機會。面對 legacy code 時,關鍵不是重寫,而是找到接縫、利用接縫。當你開始用接縫的角度看待程式碼,就能找到方法在不大幅改動的前提下將程式碼納入測試。