OkHttpのMockWebServerとRobolectricでFragmentの動作をテストする

## 何がしたいか

テストコードを書くことを考慮してないFragmentのコードに対して、テストコードを書いていきたい。既存コードを変更せずに。
テストとしては例えば、APIアクセスの結果、特定のエラーレスポンスの場合は別画面に遷移するとか。

## 既存コードの仕組み

OkHttp+RetrofitでAPIアクセスを管理しています。
また、APIの接続先が本番・ステージング・開発の3環境あるので、`strings.xml`を各build variantsのディレクトリに置いています。

ApiClientというクラスで、Retrofitのサービスインスタンスを生成しています。
（ホントは、DaggerとかでDIしたほうが良さそうですが。）

```java:ApiClient.java
public class ApiClient {
    private static ApiClient sApiClient;

    private HogeService hogeService;

    private ApiClient() {
    }

    public static ApiClient getInstance() {
        if (sApiClient == null) {
            sApiClient = new ApiClient();
        }
        return sApiClient;
    }

    public void initialize(final Context c) {
        OkHttpClient okHttpClient = new OkHttpClient().newBuilder()
                .addInterceptor(new HttpLoggingInterceptor().setLevel(BuildConfig.DEBUG ? HttpLoggingInterceptor.Level.BODY : HttpLoggingInterceptor.Level.NONE))
                .build();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(context.getString(R.string.api_base_url))
                .addConverterFactory(GsonConverterFactory.create())
                .client(okHttpClient)
                .build();

        hogeService = retrofit.create(HogeService.class);
    }

    public HogeService getHogeService() {
        return hogeService;
    }
}
```

上記のinitializeメソッドは、Application継承クラスから、onCreateのタイミングで呼びだされます。

APIを実行するFragmentの方では、クリックイベントなどを契機に、下記のように呼び出します。

```java:SomeFragment.java
ApiClient.getInstance().getHogeService().login(loginInfo).enqueue(new Callback<UserInfo>() {
    @Override
    public void onResponse(Call<UserInfo> call, Response<UserInfo> response) {
        if (response.code() == 401) {
            Intent intent = new Intent(activity, SignUpActivity.class);
            startActivity(intent);
            return;
        }
        // ログイン成功したときの処理
    }

    @Override
    public void onFailure(Call<UserInfo> call, Throwable t) {
    }
});
```

上記では、例として、「ログインAPIが401だったら登録Activityを表示する」としています。
これを **コードを変更せずに** テストコードを書いてみます。（既存コードの問題点はスルーし、テストを書くことを優先）

## 基本方針

- [Robolectric](http://robolectric.org/)を利用して、Fragmentの生成、イベントの発生を行う。
- OkHttpの[MockWebServer](https://github.com/square/okhttp/tree/master/mockwebserver)を利用して、APIレスポンスを偽装する。
- [Mockito](http://mockito.org/)やRobolectricのShadowを使い、本番コードを修正しないようにテストを書く。

## やったこと

### build.gradle

build.gradleでは、以下のようにテスト用の各種ライブラリを読み込んでいます。

```
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:1.10.19'
    testCompile "org.robolectric:robolectric:3.1"
    testCompile "org.robolectric:shadows-support-v4:3.1"
    testCompile "org.robolectric:shadows-multidex:3.0"
    testCompile "com.squareup.okhttp3:mockwebserver:3.4.1"
```

### MockWebServerと通信させるため、RetrofitのbaseUrlを書き換える

`robolectric.properties`を作成し、テスト用のApplication継承クラスを指定します。

```robolectric.properties
application=com.hoge.StubApp
sdk=23
```

また、SDKを23にすることでNoClassDefFoundErrorが発生してしまったので、GLインターフェースを定義だけしておきます。
参考: https://github.com/robolectric/robolectric-gradle-plugin/issues/145#issuecomment-95456798

```
package javax.microedition.khronos.opengles;

public interface GL {
}
```

StubAppクラス内で、Mockitoの`spy`を利用して、改ざんしたURLで`ApiClient`を初期化するメソッドを提供します。

```java:StubApp.java
public class StubApp extends Application {

    public void initializeBaseUrl(MockWebServer server) {
        StubApp spiedApp = spy(this);
        when(spiedApp.getString(R.string.api_base_version)).thenReturn(server.url("/api/1.0.0/").toString());

        ApiClient.getInstance().initialize(spiedApp);
    }
}
```

これで、`initializeBaseUrl`を実行すると、Fragment内などで取得できる`HogeService`インスタンスの向き先を変えることが出来ました。

### MockWebServerを起動・停止する

MockWebServerは`TestRule`を継承しており、`@Rule`を付けておくことで、自動で起動・停止を行ってくれます。

（以前は`MockWebServerRule`という別クラスだったようですが、バージョンアップのタイミングで統合されたようです。）

```java:SomeFragmentTest.java
@RunWith(CustomRobolectricTestRunner.class)
@Config(shadows = {ShadowOkHttpClient.class})
public class SomeFragmentTest {
    @Rule
    public MockWebServer server = new MockWebServer();

    private SomeFragment fragment;

    @Before
    public void setup() throws IOException {
        fragment = new SomeFragment();
        startFragment(fragment, SomeActivity.class);

        ((StubApp) RuntimeEnvironment.application).initializeBaseUrl(server);
    }
...
```

### APIのcallbackを即座に実行するようにする

通常、`enqueue`した`retrofit2.Callback`の各メソッドは、非同期に実行されます。
そのままだとコールバックを待たずにassertを行うことになってしまい、正しい結果が得られません。
そのため、Testクラスで設定していた `CustomRobolectricTestRunner` と `ShadowOkHttpClient` が必要になってきます。

```java:CustomRobolectricTestRunner.java
public class CustomRobolectricTestRunner extends RobolectricGradleTestRunner {
    public CustomRobolectricTestRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @Override
    protected ShadowMap createShadowMap() {
        return super.createShadowMap().newBuilder()
                .addShadowClass(ShadowOkHttpClient.class)
                .build();
    }

    public InstrumentationConfiguration createClassLoaderConfig() {
        return InstrumentationConfiguration.newBuilder()
                .addInstrumentedClass(OkHttpClient.class.getName())
                .build();
    }
}
```

```java:ShadowOkHttpClient.java
@Implements(OkHttpClient.class)
public class ShadowOkHttpClient {
    @RealObject
    OkHttpClient realObject;

    @Implementation
    public OkHttpClient.Builder newBuilder() {
        return Shadow.directlyOn(realObject, OkHttpClient.class)
                .newBuilder()
                .dispatcher(new Dispatcher(new AbstractExecutorService() {
                    private boolean shuttingDown = false;
                    private boolean terminated = false;

                    @Override
                    public void shutdown() {
                        this.shuttingDown = true;
                        this.terminated = true;
                    }

                    @NonNull
                    @Override
                    public List<Runnable> shutdownNow() {
                        return new ArrayList<>();
                    }

                    @Override
                    public boolean isShutdown() {
                        return this.shuttingDown;
                    }

                    @Override
                    public boolean isTerminated() {
                        return this.terminated;
                    }

                    @Override
                    public boolean awaitTermination(long timeout, @NonNull TimeUnit unit) throws InterruptedException {
                        return false;
                    }

                    @Override
                    public void execute(@NonNull Runnable command) {
                        command.run();
                    }
                }));
    }
}
```

`CustomRobolectricTestRunner`では、`OkHttpClient`が利用されたときに`ShadowOkHttpClient`を利用するように設定しています。
`ShadowOkHttpClient`では、`newBuilder`メソッドを書き換えています。
本来`newBuilder`で返却するはずだったものに、「即時実行するdispatcher」を設定し、それを返却しています。

### テストを記述する

```java:HogeFragment.java
public class SomeFragmentTest {
...
    @Test
    public void clickButton_showSignUpActivity() throws Exception {
        server.enqueue(new MockResponse()
                .setResponseCode(401)
                .setBody("{\"message\":\"ユーザーが存在しません。\"}"));

        fragment.getView().findViewById(R.id.some_button).performClick();

        assertThat(server.takeRequest().getPath(), is("/login"));

        Intent intent = Shadows.shadowOf(fragment.getActivity()).getNextStartedActivity();
        Intent expect = new Intent(fragment.getActivity(), SignUpActivity.class);
        assertThat(intent.filterEquals(expect), is(true));
    }
```

※実際のコードからかなり改変しているので、動かなかったらごめんなさい。

`some_button`を押した時に、前述の「ログインAPIが401だったら登録Activityを表示する」機能があると思ってください。
`performClick`で、クリックイベントを擬似的に発生させ、`getNextStartedActivity`で次に遷移するActivityを取得、それが正しいかを確認しています。

## まとめ

MockitoやRobolectricのShadowを利用することで、既存のコードを書き換えず、ある程度複雑になってしまっているFragmentのテストが出来そうです。
ただ、`newBuilder`をフックしていたりと、あまり綺麗な解決方法では無い気がしています。
テストしやすい設計って大事ですね。