iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 19
0
Software Development

Android Architecture系列 第 19

Test part 3:Repository

  • 分享至 

  • xImage
  •  

之前在Day9提過使用Dependency Injection的好處之一是更容易測試,現在就是體現的時候了。

Mocking Framework

執行單元測試時會盡可能排除測試目標以外的其他class造成的影響,方式之一是將其他class模擬(mock)出來,這些模擬出來的class會呼叫method但不會真的執行method內容,這樣就能驗證method有被呼叫且method執行的結果不會影響到當前的測試,此模式稱為mocking framework。

實現mocking framework的要素之一就是DI,如果沒有套用DI的話,在constructor中實例化的物件就會是「真的(real)」,呼叫method時會真的執行完method的內容,讓測試的困難度增加。

看個簡單的例子,repository使用DI的方式從外部取得兩個constructor parameter:資料庫DAO和Retrofit service,我們用mock模擬它們,並驗證它們有執行正確的method。

repository.search("abc")被呼叫時,用verify驗證repoDao有執行search("abc")

@Test
public void search() {
    repository.search("abc");
    verify(repoDao).search("abc");
}

至於repoDao的search("abc")執行內容正不正確,那是DAO的Test要負責的事,在repository的test中只要確定這個method有執行就表示repository的邏輯是對的。

接下來就看怎麼實現mocking framework。

Mockito

Mockito是Android中熱門的mocking framework library,官方文件也採用Mockito來做範例。

加入dependencies:

testImplementation "org.mockito:mockito-core:2.13.0"

宣告物件之後用mock(Class)初始化就好了。

@RunWith(JUnit4.class)
public class RepoRepositoryTest {

    private RepoDao repoDao;
    private GithubService githubService;
    private RepoRepository repository;

    @Before
    public void init() {
        repoDao = mock(RepoDao.class);
        githubService = mock(GithubService.class);
        repository = new RepoRepository(repoDao, githubService);
    }
}

如果Android Studio認不出mock(Class)而沒有出現import提示的話,再手動新增一下:

import static org.mockito.Mockito.mock;

@RunWith(JUnit4.class)
public class RepoRepositoryTest {

    ...
}

mock的物件可以使用Mockito一系列的驗證方式,例如開頭的範例使用verify驗證:

@RunWith(JUnit4.class)
public class RepoRepositoryTest {

    private RepoDao repoDao;
    private GithubService githubService;
    private RepoRepository repository;

    @Rule
    public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();

    @Before
    public void init() {
        repoDao = mock(RepoDao.class);
        githubService = mock(GithubService.class);
        repository = new RepoRepository(repoDao, githubService);
    }

    @Test
    public void search() {
        repository.search("abc");
        verify(repoDao).search("abc");
    }
}

這樣就是簡單的mocking framework應用方式了。

Testing Repository

我們的repository負責從資料庫和API取得資料,分為幾種情況:

  1. 搜尋資料庫並找到資料,不用呼叫API。
  2. 資料庫找不到資料,呼叫API。
  3. 呼叫API但連線失敗。

第1種情況,只要資料庫裡的資料,先看上半段認識一下新面孔:

@Test
public void search_fromDb() {
    MutableLiveData<RepoSearchResult> dbSearchResult = new MutableLiveData<>();
    when(repoDao.search("foo")).thenReturn(dbSearchResult);

    Observer<Resource<List<Repo>>> observer = mock(Observer.class);
    repository.search("foo").observeForever(observer);

    verify(observer).onChanged(Resource.loading(null));
    verifyNoMoreInteractions(githubService);
}

這邊用到了when,用途是讓我們指定method的回傳值,when(X).thenReturn(Y)表示X執行的時候,回傳值要是Y,所以上面的寫法當repoDao.search("foo")執行的時候要回傳dbSearchResult。

接著repository.search("foo")執行時會呼叫repoDao.search("foo"),而後者的回傳值我們剛剛已經指定好是dbSearchResult了,所以observer就會在onChanged事件中收到dbSearchResult。

最後用verify驗證目前為讀取中,且githubService沒有發出API連線。

完整版,下半段驗證有撈到資料時不會發出API連線:

@Test
public void search_fromDb() {
    MutableLiveData<RepoSearchResult> dbSearchResult = new MutableLiveData<>();
    when(repoDao.search("foo")).thenReturn(dbSearchResult);

    Observer<Resource<List<Repo>>> observer = mock(Observer.class);
    repository.search("foo").observeForever(observer);

    verify(observer).onChanged(Resource.loading(null));
    verifyNoMoreInteractions(githubService);

    List<Integer> ids = Arrays.asList(1, 2);
    RepoSearchResult dbResult = new RepoSearchResult("foo", ids, 2);

    MutableLiveData<List<Repo>> repos = new MutableLiveData<>();
    when(repoDao.loadOrdered(ids)).thenReturn(repos);

    dbSearchResult.postValue(dbResult);

    List<Repo> repoList = new ArrayList<>();
    repos.postValue(repoList);
    verify(observer).onChanged(Resource.success(repoList));
    verifyNoMoreInteractions(githubService);
}

建立任意的RepoSearchResult並由dbSearchResult用postValue發出,表示資料庫有撈到資料。

當撈到資料時會執行repoDao.loadOrdered(ids),其回傳值用when設置為repos,當repos用postValue更新的時候表示發送資料庫撈到的結果List<Repo>,最後就驗證observer有收到資料且API沒有啟動。

第2種情況,資料庫找不到資料,需呼叫API連線。

@Test
public void search_fromServer() {
    MutableLiveData<RepoSearchResult> dbSearchResult = new MutableLiveData<>();
    when(repoDao.search("foo")).thenReturn(dbSearchResult);

    Observer<Resource<List<Repo>>> observer = mock(Observer.class);
    repository.search("foo").observeForever(observer);

    verify(observer).onChanged(Resource.loading(null));
    verifyNoMoreInteractions(githubService);

    MutableLiveData<ApiResponse<RepoSearchResponse>> callLiveData = new MutableLiveData<>();
    when(githubService.searchRepos("foo")).thenReturn(callLiveData);

    dbSearchResult.postValue(null);

    verify(repoDao, never()).loadOrdered(any());
    verify(githubService).searchRepos("foo");
}

前面大致相同,後面用dbSearchResult.postValue(null)表示資料庫找不到資料,接著就驗證DAO沒有動作及githubService已發出連線。

這樣的寫法跟Google sample一樣,最後驗證api有發出連線即完成;另一個sample後續還測試了儲存連線資料的部分,若要跟它一樣的話需修改目前的NetworkBoundResource,因為在saveResultAndReInit中使用new AsyncTask這樣的寫法是沒辦法用Mockito測試的,要套用DI或改成其他的切換thread方式例如AppExecutors

最後一種情形是API連線失敗了,回傳error時的測試:

@Test
public void search_fromServer_error() {
    when(repoDao.search("foo")).thenReturn(AbsentLiveData.create());
    MutableLiveData<ApiResponse<RepoSearchResponse>> apiResponse = new MutableLiveData<>();
    when(githubService.searchRepos("foo")).thenReturn(apiResponse);

    Observer<Resource<List<Repo>>> observer = mock(Observer.class);
    repository.search("foo").observeForever(observer);
    
    apiResponse.postValue(new ApiResponse<RepoSearchResponse>(new Exception("idk")));
    verify(observer).onChanged(Resource.error(null, "idk"));
}

when設置repoDao.search("foo")回傳AbsentLiveData,當執行repository.search("foo")時就會因為repoDao撈出的結果是null而啟動githubService.searchRepos("foo"),最後發送帶有exception的連線結果並驗證狀態為error。


GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day19-test-repository


上一篇
Test part 2:Retrofit api calls and common util
下一篇
Test part 4:ViewModel
系列文
Android Architecture30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言