追記(2015/19/7)
Robolectric 3.0以降はJVM unit testingの仕組みを利用するようになりました。素の状態よりテストしやすいのでこちらを利用するとよさそうです。このエントリで例に上げたHeliumもRobolectricに移行しました。
三行まとめ
- Android Studio 1.1からJVM unit testingがサポートされた
- Android frameworkにあまり依存しないロジックであれば有用
- テストファイル書き換えからテスト実行まで数秒でおわるので試行錯誤しやすい
説明
Android SDKがJVMによるテストをサポートしたという話があります:
- Unit testing support - Android Tools Project Site
- Gradle Plugin 1.1.0-rc1でJVMでテストを実行出来るようになったらしい - visible true
Robolectric を使わずとも、公式のツールキットを使うだけでJVM unit testingができるというのは画期的で、今後モデルまわりのテストはこっちに行くのかなと思っています。
ただし、まだAndroid Studioで通常のandroidTestとの併用は難しいですし、テストレポートをAndroid Studioで見ることすらできません。プロダクトに取り入れる段階ではないでしょう。
またAndroid frameworkは一切使えません。Android frameworkのクラスライブラリを参照することはできるのですが、何か機能を呼ぼうとすると軒並みthrow new RuntimeException("Method foo not mocked")
となります。
ともあれ、どんなものかは試しておきたいところ。というわけでテストを書いてみました。
もともとのアプリは以下のような特徴をもっています。
- RetrofitでWeb APIにアクセスする
- RetrofitのRxJavaインターフェイスを使用
- Dagger 2.0 (2.0-SNAPSHOT) でDependency Injection
- JUnit4でテストを書いている
- Circle-CI + DockerでCI
いざテストを書いてみると、DIをしているのでmockするのは難しくありませんでした。RetrofitはHTTP clientを retrofit.client.Client
というインターフェイスで抽象化しているので、mockwebserverすら必要ありません。 Client#execute()
のmockを実装するだけです。
class MockClient implements Client {
final String path;
final String assetName;
final String contentType;
MockClient(String path, String assetName, String contentType) {
this.path = path;
this.assetName = assetName;
this.contentType = contentType;
}
@Override
public Response execute(Request request) throws IOException {
URI uri = URI.create(request.getUrl());
if (uri.getPath().equals(path)) {
return resourceFoundInXml(request.getUrl());
} else {
return resourceNotFound(request.getUrl());
}
}
Response resourceNotFound(String uri) {
return new Response(uri, 404, "not found", new ArrayList<Header>(),
new TypedByteArray(contentType, new byte[]{}));
}
Response resourceFoundInXml(String uri) throws IOException {
return new Response(uri, 200, "ok", new ArrayList<Header>(),
new TypedByteArray(contentType, getAssetFileInBytes(assetName)));
}
}
@Test
public void testRequestHotentries() throws Exception {
HatebuFeedClient feedClient = new HatebuFeedClient(
new MockClient("/hatena/b/hotentry", "hotentries.rss", "application/xml"),
new MockRequestIntercepter());
List<HatebuEntry> entry = feedClient.getHotentries().toBlocking().single();
assertThat(entry, hasSize(greaterThan(0)));
}
なお、アプリ実装のClientのproviderはOkHttpを使うもので、以下のようになっています。
@Provides
public Client provideRetrofitClient(OkHttpClient httpClient) {
return new OkClient(httpClient);
}
Android frameworkがまったく使えないので、MockContextを駆使したり
Log.d()
の代わりに System.out.println()
を使ったりと、通常のandroidTest以上にpure Javaよりに意識を切り替える必要はあります。モデルやAPI通信部分、ユーティリティであればかろうじてテストできるという感じでしょうか。
しかしながら、テストを実行する時間が非常に短いというのは大きなメリットで、試行錯誤しながらロジックを書いていくというケースでは今後使っていきたいという印象です。