今天測試ViewModel會比較輕鬆,一來ViewModel本身邏輯比較簡單,只跟repository互動所以要mock的物件少,二來苦痛都在前兩天經歷了,把學會的東西拿出來用就好。
ViewModel的code比較短所以貼在這方便待會對照。
public class RepoViewModel extends ViewModel {
private final MutableLiveData<String> query = new MutableLiveData<>();
private final LiveData<Resource<List<Repo>>> repos;
private RepoRepository mRepoRepository;
@Inject
public RepoViewModel(RepoRepository repoRepository) {
super();
mRepoRepository = repoRepository;
repos = Transformations.switchMap(query, new Function<String, LiveData<Resource<List<Repo>>>>() {
@Override
public LiveData<Resource<List<Repo>>> apply(String userInput) {
if (userInput == null || userInput.isEmpty()) {
return AbsentLiveData.create();
} else {
return mRepoRepository.search(userInput);
}
}
});
}
LiveData<Resource<List<Repo>>> getRepos() {
return repos;
}
void searchRepo(String userInput) {
query.setValue(userInput);
}
}
起手式一樣是加入@Rule
及mock物件。
@Rule
public InstantTaskExecutorRule instantExecutor = new InstantTaskExecutorRule();
private RepoViewModel viewModel;
private RepoRepository repository;
@Before
public void init() {
repository = mock(RepoRepository.class);
viewModel = new RepoViewModel(repository);
}
@Test
的部分就比較自由可以依需求寫,例如ViewModel實例化時constructor中的repos有沒有成功設置:
@Test
public void testNotNull() {
assertThat(viewModel.getRepos(), notNullValue());
verify(repository, never()).search(anyString());
}
當LiveData沒有observer時不會呼叫repository進行搜尋:
@Test
public void dontFetchWithoutObservers() {
viewModel.searchRepo("foo");
verify(repository, never()).search(anyString());
}
接著是今天唯一有用新東西的地方,當LiveData有observer時驗證ViewModel的搜尋關鍵字和repository的關鍵字相同:
@Test
public void fetchWhenObserved() {
ArgumentCaptor<String> input = ArgumentCaptor.forClass(String.class);
viewModel.getRepos().observeForever(mock(Observer.class));
viewModel.searchRepo("foo");
verify(repository, times(1)).search(input.capture());
assertThat(input.getValue(), is("foo"));
}
新面孔ArgumentCaptor
可以用來驗證argument value,在search的時候用capture()
把value存下來,接著assertThat(input.getValue(), is("foo"))
驗證存的value是不是"foo",是就表示search的argument是"foo"。
使用ArgumentCaptor
來驗證argument的好處是程式闡述性比較高,capture()
和gerValue()
會比直接寫字串"foo"更容易表明用意。
另一個新面孔times(int)
表示期望的執行次數,上例中驗證search只被執行一次。
兩者可以搭配起來測試執行多次的method,因為capture()
能夠執行多次,每一次的value都會存下來變成List並於之後用getAllValues()
取得,加上times(int)
就能同時驗證執行次數和每次的argument value。
最後一個是測試input為null時的處理,沒有用到新的東西。
@Test
public void nullSearchInput() {
Observer<Resource<List<Repo>>> observer = mock(Observer.class);
viewModel.searchRepo("foo");
viewModel.searchRepo(null);
viewModel.getRepos().observeForever(observer);
verify(observer).onChanged(null);
}
今天內容比較少都是code在充版面,因為明天View的測試會用Espresso在Android裝置上運行,整體實作方式不同所以另外寫。
GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day20-test-viewmodel