非同步測試(Callback)

注意事項

  1. 隔離外部資源
    • 不要用實際的東西去測,應該用 mock framework 做一個假的外部資源
  2. 使用同步模擬非同步
    • 單元測試裡不能讓 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);
    }

這時候再一次執行測試,測試通過!

results matching ""

    No results matching ""