上一篇透過控制依賴的內容讓受測對象走入不同分支。這一篇要介紹另一種控制依賴的方式:使用 Mock 工具。

不受控的依賴#

並不是每個依賴都這麼好控制。實務上常見的麻煩情境包括:

  • 依賴於即時時間
  • 依賴於亂數
  • 依賴於外部資料
  • 依賴的行為又受到第二、第三層依賴的影響

這些情境共同的特徵是:依賴的行為很難或無法直接給值控制。當介面設計或結構不夠理想時,這類情況就會被放大。

延續先前的獎學金例子,獎學金需要經過申請。系統收到申請單時得先檢查是否還在申請期限內:「今天若在申請截止日當天或之前,該申請通過時間審核;否則不通過。」程式長這樣:

public boolean checkTime(Application application) {

    Scholarship scholarship = scholarshipRepository.find(application.getScholarshipId());

    LocalDate deadline = scholarship.getDeadline();

    LocalDate today = LocalDate.now();

    return today.isEqual(deadline)
            || today.isBefore(deadline);

}

這段邏輯很單純,但要測卻不容易。它對外有三個依賴:ApplicationScholarshipRepositoryLocalDate

Application 透過建構子或 setter 即可控制;但另外兩個就棘手了:

  • 沒有真正的資料庫,無法直接控制 ScholarshipRepository.find() 的回傳值
  • LocalDate.now() 隨時都會變,無法直接決定它回傳什麼

Mockito 登場#

針對這種情境,Java 的開源套件 Mockito 是最常用的工具之一。它能依照我們的指示,產出一個假的物件來餵給受測對象。這個假物件回傳什麼、怎麼回傳,都由我們指定。

ScholarshipRepository 為例,可以這樣做:

ScholarshipRepository fakeRepository = Mockito.mock(ScholarshipRepository.class);
Mockito.when(fakeRepository.find(777L)).thenReturn(new Scholarship(LocalDate.MAX));

這段意思是:建立一個假的 ScholarshipRepository,跟它約定好「之後若有人來查 id 為 777 的 Scholarship,就回傳一個截止日為 LocalDate.MAX(天長地久)的物件」。如此就不必真的搬一台資料庫來測。

關於樁件(Stub)與模擬物件(Mock)兩個名詞,本系列暫時把它們都視為「能照我們指示行動的假物件」就好,細節差異稍後再深究。

Mockito 也搞不定的情況#

Mockito 雖然強大,仍有它搞不定的場景,主要是「設計得不夠好」的程式。以前面的 LocalDate.now() 為例,這是一個 static 方法,會直接讓邏輯與系統狀態強耦合。

當邏輯隨時間不同而走不同分支時,是不可能讓某些測項只能早上跑、某些只能晚上跑的。系統時間不該由測試控制這件事直接耦合進業務邏輯,本身就是設計上的警訊。

也因此,早期的 Mockito 並不支援 mock static 方法,立場是希望開發者把依賴反轉做好,將需要假 static 方法的情況降到最低。

那當時開發者怎麼測?有人會在測試旁建立一個跟原類別同 package、同名稱的類別,藉由編譯期載入順序覆蓋掉原行為。這算可行的偏方,但顯然麻煩。

Java 專屬救命神器:PowerMock#

PowerMock 之所以誕生,就是要解決這類困境。它能蓋掉 private 成員,也能取代 static 方法,碰到不得已的情況的確很救命。例如 LocalDate.now() 可以用 PowerMock 來覆蓋。

但 PowerMock 是「救命用」的工具,不建議當常規武器使用。當開發者太依賴它,就容易疏於思考耦合度與依賴反轉,反而更容易寫出結構糟糕的程式。PowerMock 官方也說過:把它放到資淺工程師手上,反而可能弊大於利。

Mockito 的反擊#

後來 Mockito 也不再堅持立場。從 3.4.0 之後,Mockito 開始支援 static 方法的覆蓋。對於以前硬邦邦難測的 static method,這個功能讓 Mockito 如虎添翼:

LocalDate expected = LocalDate.of(2029, 12, 31);
Mockito.mockStatic(LocalDate.class).when(LocalDate::now).thenReturn(expected);

治本:做好依賴反轉#

但其他語言怎麼辦?最終還是要回頭面對「設計」這件事。如果每次都得仰賴特定語言的特定工具來解決耦合問題,一旦換個語言或工具不再維護,問題又會回來。

真正的根治辦法是把依賴注入(Dependency Injection)做好。例如把程式改成這樣:

public boolean checkTime(Application application, MyClock clock) {
    LocalDate today = clock.now();
    // 以下省略 ...
}

業務邏輯不再直接耦合到系統時間,測試與正式環境只要傳入不同的 MyClock 實作即可。整體易測性、可讀性都會更好。

「不好測」常常是個警訊,意味著「不好用、不好改」。發現問題就早點處理,畢竟下一個維護者很可能就是三個月後的自己。

聽話的用真的,不聽話的用假的#

連續兩篇分別示範了兩種控制依賴的手法:直接給值、與用 Mock 工具產生假物件。何時該用哪一種?並沒有強制規定,但可以參考一個簡單原則:「聽話的依賴用真的,不聽話的用假的」。

只要能順利讓受測對象走到目標分支,混搭真假物件也沒問題。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10260707