iT邦幫忙

2021 iThome 鐵人賽

DAY 6
2
Software Development

你就是都不寫測試才會沒時間:Kuma 的 30 天 Unit Test 手把手教學,從理論到實戰 (Java 篇)系列 第 6

Day 06 「不聽話就換掉」測試與依賴:測資料 之 用 Mock 工具控制依賴

2021 IT 鐵人 Day 06 測試與依賴:測資料 之 用 Mock 工具控制依賴

今天要來聊的是另一種控制依賴的方法:Mock 工具。

不受控的依賴

讀者也許會覺得很奇怪。就算有依賴又怎樣?上一篇不就有提到,如何透過控制依賴的行為,來決定受測對象要走哪條邏輯分支,進而完成單元測試嗎?還有什麼好講的?

的確,當依賴的行為容易控制時,這樣做的確就夠了,實現起來也不難。然而世事並非都是這麼簡單,有時候就是會遇到依賴很難控制的情況,譬如:

  1. 依賴於即時時間
  2. 依賴於亂數
  3. 依賴於外部資料
  4. 依賴的行為又受到第二層、第三層、或第四層依賴的影響。

這前幾種情況有些表現出了程式對系統的直接依賴,而有些則表現出我們自己的程式的元件之間相依性太強。總而言之,當程式介面設計或是結構有問題時,就會導致依賴的行為難以控制,甚至是無法控制。

舉個例子看看。在上一篇我們做了計算獎學金的功能,但獎學金是要申請的。每個申請單送進系統的時候,我們得先審核一下資格是否符合。但在那之前最重要的,就是今天「過了申請日了沒」!如果申請人太晚申請,哪怕對方條件再好,我們也得忍痛拒絕,以示公平,對吧?

所以,邏輯很簡單:「如果今天是本獎學金申請截止日之前,這張申請單就通過時間的審核;反之則不通過。」

邏輯簡單,程式應該也很簡單吧?看看:

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);

}

喔,真的蠻簡單的耶,那趕快來測吧!

「那個... 怎麼測?」

不太好測,對吧?上一篇提到了,我們可以藉由控制依賴的行為,來決定受測程式邏輯要怎麼走,進而驗測正確性。然而,這段小小的程式碼,對外有三個依賴,分別是 Application、ScholarshipRepository,以及 LocalDate。

Application 好解決,直接使用 setter 或 constructor 就可以控制其 getter 的行為,但另外兩個依賴就沒那麼聽話了。你將會面臨兩個問題:

  1. 無法控制 Repository 的回傳值,因為沒有真正的 Database
  2. 無法控制 LocalDate.now() 回傳的值,因為測試隨時都可能被執行

這兩個都不是直接給值就能解決的問題。對此,坊間有一些常用的手法可以間接地控制他們,譬如抽介面給假實作,或是繼承原類別後定義新行為。而這裡,我想介紹的是筆者自己比較習慣用,做起來也比較直接的方法:使用 Mock 工具。

Mock 工具粉墨登場:Mockito 套件

現在已經很多語言都有自己的 Mock 套件了,而在 Java 中,Mockito 是一個非常常用的開源套件。它可以直接生成一個 Stub 出來餵給你的受測對象,但是卻完全依照你要求的方式表現。亦即,回傳你要它回傳的任何值。

以上面的例子,ScholarshipRepository 的實作會去連真正的 DB,但這件事我們不希望發生,於是就可以透過 Mockito 來產生一個「假的」物件來頂替,讓我們的受測對象與其互動。透過控制此假物件的行為,來決定受測對象要走哪條邏輯分支。譬如:

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

在上例中,我們製造了一個假的 ScholarshipRepository,並且跟他串通好說,待會如果有人跟他要 id 為 777 的 Scholarship 物件,他就要回傳我偷偷塞給他,期限為「天長地久(LocalDate.MAX)」的這一個 Instance。如此一來,我就可以輕鬆控制 ScholarshipRepository 這個依賴,而不用真的準備一台活著的 DB 了。

註:關於 Mock 與 Stub 的區別,筆者不會在此系列文章中詳述,讀者可以暫時把他們都視為一種能照我們要求行動的「假物件」,先這樣就可以了。

Mockito 也搞不定的情況

Mockito 這麼厲害,有沒有搞不定的情況?有啊!就是「寫得比較差的程式」。

這當然有些開玩笑成分,但的確有部分為真。譬如前面這個例子,在程式裡直接呼叫了一個 static 方法 LocalDate.now(),就會直接讓我們的程式與系統狀態產生很強的耦合。

先不說別的,當你邏輯分支會隨著時間不同而有所不同時,難不成你有些測項早上才能跑,有些晚上才能跑?因為系統時間不是我們能控制的,把我們的邏輯與之直接耦合,並不是一個好主意。正因如此(當然也有其他原因啦),Mockito 最一開始是不支援對 static 方法的。他們認為我們應該要好好做好 Dependency Inversion,讓需要「假 static 方法」的情況將到最低。

那怎麼辦呢?其實開發者也都很聰明,還是有辦法。在很久很久以前,有一些人為了要測試 static 方法,在測試旁邊寫一個 package 與 類別名都一樣的類,這樣 Unit Test 在編譯期間就可以先載入你新定義這個類,原類的行為就被蓋掉了。

我自己沒這麼試過,但看起來的確是有一些成功案例就是了。

Java 專屬救命神器:PowerMock

上述方法雖然可行,但這聽起來很麻煩對吧?是啊。於是有群人,為了這種情況發明了另一套工具,命名為 PowerMock。

PowerMock 正如其名地非常強大,可以蓋掉 private 成員,也可以取代 static 方法,在不得已的時候,還是蠻好用的。譬如上述的 LocalDate.now(),就可以用 PowerMock 來取代。

但是,PowerMock 是救命用的,非不得已,筆者不建議各位使用,他會使得開發原對於程式耦合度與控制反轉變得不夠上心,反而容易寫出結構糟糕的程式。他們自己官網也是這麼說的:“Putting it in the hands of junior developers may cause more harm than good.”

大反轉:不甘示弱的 Mockito

然而,人家 Mockito 也不是吃素的,眼看大家還是執迷不悟(誤),他們也不再堅持,終於,在 3.4.0 以後,Mockito 也開始支援 static 方法的覆蓋了。加了這個新功能之後,Mockito 如虎添翼,遇到以前硬邦邦測不動的 static method,也開始可以自由操控了,像這樣:

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

治本:做好控制反轉

「那,其他語言怎麼辦呢?」

是啊,最終,我們還是依賴了「特定語言的特定工具」,才解決了我們設計上耦合度太高,導致不好測的問題,今天如果要換個其他語言,或是該工具不再更新,那我們設計不良的結構,很快就又會回過頭來造成不良影響。說到底,要徹底的解決程式「不好測」的問題,筆者認為,我們還是得回歸物件導向的初衷,把 Dependency Inversion 的工作做好。

譬如,把程式寫成這樣:

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

不就很好測了嗎?又可以把對系統細節的依賴,隔絕在核心邏輯之外,很方便吧?

俗話(?)說得好,「不好測的東西,代表不好用、不好改」,測試不好寫,其實是一個警訊,他正在告訴我們「設計應該有點問題」。既然發現有問題,那還是早點改掉得好。畢竟,我們也不能光是解決自己好測的問題,還是得為我們程式的使用者,以及未來的修改者著想。更何況,這兩種角色都極有可能是三個月後的我們自己,對吧?

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

連續兩篇,我們聊了如何透過對依賴的控制,來讓受測目標依照我們的想法去走某個邏輯分支,進而檢驗在特定情況下,程式表現是否正確。那麼究竟何時要用「假的」,何時要用「真的」呢?雖然沒人規定,但依筆者自身經驗,大概可以依一個準則判斷:「聽話的用真的,不聽話的用假的」。

反正目的是讓受測對象照我們想法走,只要能簡單方便地控制依賴的行為,真的假的,或是真假混用,都無所謂的。

謎之聲:「管他黑貓白貓,能抓到老鼠的就是好貓。」

Reference

  1. Martin Fowler 對 Mock 與 Stub 的解釋: https://martinfowler.com/articles/mocksArentStubs.html
  2. Mockito:https://site.mockito.org/
  3. Powermock:https://github.com/powermock/powermock
  4. Mockito 對於 static 的支持:https://javadoc.io/static/org.mockito/mockito-core/3.12.4/org/mockito/Mockito.html#static_mocks
tags: ithelp2021

上一篇
Day 05 「乖,聽話給你吃糖果!」測試與依賴:測資料 之 用資料控制依賴
下一篇
Day 07 「Tell. Don't Ask.」 測試與依賴:測行為
系列文
你就是都不寫測試才會沒時間:Kuma 的 30 天 Unit Test 手把手教學,從理論到實戰 (Java 篇)30

尚未有邦友留言

立即登入留言