今天開始會講個幾天的Test,回顧一下App架構圖:
Guide to App Architecture
接下來會把各區塊的Test都寫一下,從底層開始往上寫。Room已經在昨天完成了,今天會寫Retrofit的測試,並對之後測試做一點前置準備。
API的測試通常會用mock的方式模擬出server和連線,除了避免對正式機發出連線以外,也可以自行編寫response內容以便驗證。另外我們的Retrofit使用了LiveDataCallAdapter,連線回傳結果會是LiveData,在測試上也會稍有不同。
1.加入dependencies
我們使用MockWebServer來模擬api連線:
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
加入Architecture Component的testing library,因為測試時我們希望LiveData是同步(synchronously)執行,否則會在收到結果前就驗證而導致失敗,此testing library有提供@Rule
讓LiveData同步執行。
testImplementation "android.arch.core:core-testing:1.0.0"
2.建立response內容
將API會回傳的內容存成檔案以供MockWebServer讀取。我們要測試的是GitHub API的search repositories連線,以我們要的搜尋關鍵字呼叫該連線並將response內容存下來,例如https://api.github.com/search/repositories?q=yigit ,將結果儲存為search.json,放在src > test > resources > api-response路徑下。
3.testing
測試連線不用在Android裝置上執行,因此我們在test路徑下建立test case。
關鍵的地方是加入@Rule
讓LiveData同步執行。
@Rule
public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
MockWebServer直接用new建立,並將Retrofit的baseUrl改成mockWebServer.url("/")
就完成模擬server了,記得在@After
中關閉。
private GithubService githubService;
private MockWebServer mockWebServer;
@Before
public void createService() throws IOException {
mockWebServer = new MockWebServer();
githubService = new Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
...
.create(GithubService.class);
}
@After
public void stopService() throws IOException {
mockWebServer.shutdown();
}
完整程式:
@RunWith(JUnit4.class)
public class GithubServiceTest {
@Rule
public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();
private GithubService githubService;
private MockWebServer mockWebServer;
@Before
public void createService() throws IOException {
mockWebServer = new MockWebServer();
githubService = new Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(new LiveDataCallAdapterFactory())
.build()
.create(GithubService.class);
}
@After
public void stopService() throws IOException {
mockWebServer.shutdown();
}
@Test
public void search() throws IOException, InterruptedException {
enqueueResponse("search.json");
ApiResponse<RepoSearchResponse> response = getValue(
githubService.searchRepos("foo"));
assertThat(response, notNullValue());
assertThat(response.body.getTotal(), is(41));
assertThat(response.body.getItems().size(), is(30));
}
private void enqueueResponse(String fileName) throws IOException {
InputStream inputStream = getClass().getClassLoader()
.getResourceAsStream("api-response/" + fileName);
BufferedSource source = Okio.buffer(Okio.source(inputStream));
MockResponse mockResponse = new MockResponse();
mockWebServer.enqueue(mockResponse
.setBody(source.readString(StandardCharsets.UTF_8)));
}
}
enqueueResponse(String fileName)
中我們讀取search.json,接著mockWebServer會用enqueue
將json內容存起來,這樣下一個call的response就會是此內容。
githubService.searchRepos("foo")
為發出的call,得到的response即為存於mockWebServer的search.json內容,接著我們就可以驗證response的結果與json文件是否一致。
其中有一個問題是getValue()
,這是昨天建好的LiveDataTestUtil的method,但昨天我們是建立在androidTest中,今天則是在test下作業所以會抓不到getValue()
。
對於這些共用的Util,我們建一個新的test-common路徑存放,將來測試才方便。
由於test case會依是否需在Android裝置上執行而分別放在androidTest和test兩個路徑,但有時會有共用的功能,例如剛剛提到的LiveDataTestUtil或是建立新的Repo這種到處都用到的method,我們就可以建立新的test-common路徑來存放。
於src資料夾當中建立test-common資料夾,後續都跟test一樣,例如src\test-common\java\ivankuo\com\itbon2018\
。
接著必須在build.gradle
中為androidTest和test加上該路徑,這樣它們兩個才能讀到新路徑中的程式,如果沒加這段的話會Android Studio不會引用新的路徑,不能使用新路徑中的程式且在package那邊也看不到:
android {
...
sourceSets {
...
androidTest.java.srcDirs += "src/test-common/java"
test.java.srcDirs += "src/test-common/java"
}
}
接著就可以在test-common中加入Util了,例如TestUtil讓我們建立Repo時方便一點:
public class TestUtil {
public static Repo createRepo(String owner, String name, String description) {
return createRepo(1, owner, name, description);
}
public static Repo createRepo(int id, String owner, String name, String description) {
return new Repo(id, name, owner + "/" + name,
description, new Owner(owner, null, null), 3);
}
}
用Project檢視方式看package會長這樣:
第一層依序有androidTest、main、test、test-common,其中test裡有resources資料夾存放json文件。
用Android檢視方式就會好看一點了:
GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day18-test-retrofit