今天來聊「不回傳值的命令」的使用場景與測試。
程式行為,大多不外乎 Query 或 Command。所謂的 Query,就是向某個物件問東西,譬如向 Student 問 FullName、向 Transcript 問 AverageScore、向 Course 問 Description 等等。而 Command 則是叫物件去做某件事。譬如叫 Student 去 Register、叫 Semester 去 Start、叫 EmailService 去 Send 等等。
私以為,物件導向,就是「你現實生活會怎麼做,你程式就這樣寫。」在現實生活中,我們叫別人做事,有時候會期待對方簡單回覆執行狀況如何,有時候不會,讓對方有事再回報就好。在物件導向的程式設計中也是一樣,大部分的 Command,如果凡事正常,對方要嘛不回傳任何訊息,要嘛只是簡單回個 boolean 代表正確與否、或回個 int 代表影響資料筆數。最多,就是在發生錯誤時,丟個 Exception 出來。
無論如何,原則應該是要「簡單就好」,因為我就是不想管那麼多,才把事情委託另一個物件去辦嘛!都委託給別人了,我當然只要知道有沒有成功就好啦!我管那麼多細節做什麼?
物件導向程式設計有這麼一句名言:"Tell. Don't Ask." 就是在講這樣的設計概念。
你說這跟單元測試有什麼關係?有關係。遵照上述原則設計時,函式的呼叫方幾乎不會預期得到什麼回信。如果我要測試的對象,會至少回傳某個值,在對方執行完畢後,我可以去驗那個值是否正確,但萬一對方什麼都沒回(void)呢?我要怎麼驗證他有做到他答應我的事?
這就是我們今天要聊的主題:測行為。
在前兩篇中,我們檢查過了學生獎學金的申請日期。如果這是唯一的條件(範例而已嘛,意思到了就好),那檢查完後就能將這份申請書存起來(假設就存在資料庫),這時申請便通過了。
我們來看看程式長怎樣:
public class ApplyScholarshipService {
private final ApplicationChecker checker;
private final ApplicationRepository applicationRepository;
public ApplyScholarshipService(ApplicationChecker checker, ApplicationRepository applicationRepository) {
this.checker = checker;
this.applicationRepository = applicationRepository;
}
public void apply(Application application) {
if (this.checker.checkTime(application)) {
this.applicationRepository.create(application);
}
}
}
看起來蠻短的,邏輯也很單純,就是:「如果申請表通過了 checker 的檢查,就叫 repository 把申請表存起來。」寫完程式,該來考慮測試怎麼寫了。
Checker 的行為因為有時間的因素,不太好控制,所以可以用我們上一篇練習過的 Mock 工具,讓它按我們意願,回傳 true 或 false。回傳 true 時,因為 repository 沒有回傳值,所以我們要來驗證他「有沒有被呼叫 create 一次」。在 Java 使用 Mockito 的話,就可以搭配 verify 的 API,寫起來就像這樣:
@Test
void check_ok_then_create() {
// 準備申請表
Application application = new Application(777L);
// 準備假 checker
ApplicationChecker checker = mock(ApplicationChecker.class);
when(checker.checkTime(application)).thenReturn(true);
// 準備假 repository
ApplicationRepository repository = mock(ApplicationRepository.class);
// 執行
new ApplyScholarshipService(checker, repository).apply(application);
// 驗證:真的有 create 一次
verify(repository, times(1)).create(application);
}
這樣就測完了。此時我們證明了,一旦 check 的檢查通過了,我們的 service 真的會叫 repository 去 create 一次。接著,下一個測項應該就是 checker 檢查不通過而回傳 false 的情形了吧?在此情況下,測試應該要大同小異,只差在最後要 verify 的是「create 從未被呼叫」。
礙於篇幅,我就不貼上來了。 github 上有完整測項,讀者可以下載來看看。
ithelp2021