非同步測試(Callback)
注意事項
- 隔離外部資源
- 不要用實際的東西去測,應該用 mock framework 做一個假的外部資源
- 使用同步模擬非同步
- 單元測試裡不能讓 main thread 去等待 background thread 完成之後再進行下一個測試,這個違反了FAST原則
範例
現在假設有一個登入的 API 操作
public class ServerAPI{
public void login(String name, String pwd, Callback callback){
//這裡模擬實際上的操作
new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
callback.onComplete(new UserInfo("john", 20, "Taipei city"));
}
}).start();
}
public interface Callback{
void onComplete(UserInfo userInfo);
}
}
而使用這 API 時可能會像這樣
public class LoginPresenter{
private ServerApi serverApi;
private String loginName;
private String loginPwd;
private Database db;
public void onLoginClick(){
serverApi.login(loginName, loginPwd, new Callback(){
@Override
public void onComplete(UserInfo userInfo){
db.saveUserInfo(userInfo);
}
});
}
}
目標
在這裡我們的目的是驗證 LoginPresenter 是否有確實將callback 拿回來的資料儲存到Database。
開始測試
首先建立一個簡單的測試案例
public class LoginPresenterTest {
@Mock
private ServerApi serverApi;
@Mock
private Database db;
@Test
public void login_should_save_userInfo_to_db() throws Exception {
LoginPresenter presenter = new LoginPresenter();
//卡住了無法繼續進行下去
}
}
在這時候就會發現你無法控制 presenter 的 serverApi 以及 db 而無法寫測試,這時候有幾種做法,先採用最簡單的:於建構子中帶入
public class LoginPresenter {
...
public LoginPresenter(Database db, ServerApi serverApi){
this.serverApi = serverApi;
this.db = db;
}
...
}
現在完成了第一步,讓我們繼續完成測試
public class LoginPresenterTest {
@Mock
private ServerApi serverApi;
@Mock
private Database db;
@Test
public void login_should_save_userInfo_to_db() throws Exception {
//arrange
MockitoAnnotations.initMocks(this);
LoginPresenter presenter = new LoginPresenter(db, serverApi);
//act
presenter.onLoginClick();
//assert
Mockito.verify(db).saveUserInfo(Mockito.any());
//在這裡驗證db是否有被確實呼叫
}
}
完成!!執行測試
可是這時候卻發現測試失敗,原因是“Wanted but not invoked”,代表 db 並沒有正確的執行saveUserInfo。因為實際上這是非同步操作,在回傳資料前測試就已經完成了測試
讓測試通過
在這個測試案例中,我們要驗證的是 LoginPresenter 的邏輯,所以可以自行實作ServerApi 的行為。
Mockito API 說明
- T when(T mock)
- 模擬指定類別行為的前置動作,以本例來說 : when(serverApi).login(...)。就是在描述serverApi.login觸發時的行為
- Stubber doAnswer(Answer answer)
- Answer 為想要完成的實作內容,在這裡想要做的是馬上呼叫 callback.onComplete 來同步的完成非同步測試
@Test
public void login_should_save_userInfo_to_db() throws Exception {
//arrange
MockitoAnnotations.initMocks(this);
UserInfo verifyData = new UserInfo("john", 20, "taipei city");
LoginPresenter presenter = new LoginPresenter(db, serverApi);
Mockito.doAnswer(invocation -> {
ServerApi.Callback callback = (ServerApi.Callback) invocation.getArguments()[2];
//拿login的第三的參數來轉型
callback.onComplete(verifyData);
return null;
})
.when(serverApi)
.login(Mockito.anyString(), Mockito.anyString(), Mockito.any(ServerApi.Callback.class));
//在這裡無法預期login三個參數的實際內容
//act
presenter.onLoginClick();
//assert
Mockito.verify(db).saveUserInfo(verifyData);
}
這時候再一次執行測試,測試通過!