之前在Day9提過使用Dependency Injection的好處之一是更容易測試,現在就是體現的時候了。
執行單元測試時會盡可能排除測試目標以外的其他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是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應用方式了。
我們的repository負責從資料庫和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