Dagger2とRetrofitを使うAndroid appをJVM unit testingしてみた

## 追記（2015/19/7)

[Robolectric](http://robolectric.org/) 3.0以降はJVM unit testingの仕組みを利用するようになりました。素の状態よりテストしやすいのでこちらを利用するとよさそうです。このエントリで例に上げた[Helium](https://github.com/gfx/Android-Helium)もRobolectricに移行しました。

## 三行まとめ

* Android Studio 1.1からJVM unit testingがサポートされた
* Android frameworkにあまり依存しないロジックであれば有用
* テストファイル書き換えからテスト実行まで数秒でおわるので試行錯誤しやすい

## 説明

Android SDKがJVMによるテストをサポートしたという話があります：

* [Unit testing support - Android Tools Project Site](http://tools.android.com/tech-docs/unit-testing-support)
* [Gradle Plugin 1.1.0-rc1でJVMでテストを実行出来るようになったらしい - visible true](http://sys1yagi.hatenablog.com/entry/2015/02/06/154430)

Robolectric を使わずとも、公式のツールキットを使うだけでJVM unit testingができるというのは画期的で、今後モデルまわりのテストはこっちに行くのかなと思っています。

ただし、まだAndroid Studioで通常のandroidTestとの併用は難しいですし、テストレポートをAndroid Studioで見ることすらできません。プロダクトに取り入れる段階ではないでしょう。

またAndroid frameworkは一切使えません。Android frameworkのクラスライブラリを参照することはできるのですが、何か機能を呼ぼうとすると軒並み`throw new RuntimeException("Method foo not mocked")` となります。

ともあれ、どんなものかは試しておきたいところ。というわけでテストを書いてみました。

* [Android-Helium/ApiTest.java at master · gfx/Android-Helium](https://github.com/gfx/Android-Helium/blob/master/app%2Fsrc%2Ftest%2Fjava%2Fcom%2Fgithub%2Fgfx%2Fhelium%2FApiTest.java)

もともとのアプリは以下のような特徴をもっています。

* 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を実装するだけです。

```java
  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を使うもので、以下のようになっています。

* [Android-Helium/AppModule.java at master · gfx/Android-Helium](https://github.com/gfx/Android-Helium/blob/master/app%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgithub%2Fgfx%2Fhelium%2Fmodel%2FAppModule.java#L73-L76)

```java
    @Provides
    public Client provideRetrofitClient(OkHttpClient httpClient) {
        return new OkClient(httpClient);
    }
```

Android frameworkがまったく使えないので、MockContextを駆使したり
`Log.d()` の代わりに `System.out.println()` を使ったりと、通常のandroidTest以上にpure Javaよりに意識を切り替える必要はあります。モデルやAPI通信部分、ユーティリティであればかろうじてテストできるという感じでしょうか。

しかしながら、テストを実行する時間が非常に短いというのは大きなメリットで、試行錯誤しながらロジックを書いていくというケースでは今後使っていきたいという印象です。