Daggerとretrofitでサーバのレスポンスを差し替える

## サーバのモックが必要
リスト表示でデータが取って来れなかったとき，
正しくEmptyViewが表示されるかのテストを書くことになりました．

![emptyview.png](https://qiita-image-store.s3.amazonaws.com/0/80986/4a2602cd-6fbb-6130-ec76-c025bc027dd7.png)



そこで，`Dagger`と`retrofit`を使って，サーバのレスポンスをテストコードから，
操作できるようにしました．こんな感じです．

```java:ImageListFragmentTest.java
@Test
public void testEmptyView_show() {
    String empty_response = 
        "{\"photos\":{\"page\":1,\"pages\":25,\"perpage\":20,\"total\":500,\"photo\":[],\"stat\":\"ok\"}";
    setupMockServer(empty_response);

    // start Activity manually
    MainActivity activity = activityRule.launchActivity(new Intent());

    // check if empty view is visible
    // ...
```
欲しいレスポンス(`empty_response`)を，今回作った`setupMockServer(empty_response)`でセットすると，
そのテスト中のAPIアクセスのレスポンスは`empty_response`が返るようになってます．

そのことについて書きます．

## サーバとの連携部分の確認
アプリのサーバとの連携部分は`retrofit`を使ってるので，こんな感じです．

```java:MyService.java
public interface MyService {

    @GET(...URL...)
    void getPopularPhotos(@Query("page") int page, Callback<ListItem> cb);

    @GET(...URL...)
    void getPhotoInfo(@Query("photo_id") String id, Callback<PhotoInfo> cb);

    //省略

}
```

`MyService`は，アプリのどこからでもアクセスできるよう
自作Applicationクラスに`static`で置いてます．

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

    private static RestAdapter rest_adapter =
            new RestAdapter.Builder().setEndpoint(ServerURL.END_POINT).build();

    public static MyService api = rest_adapter.create(MyService.class);
    
    // 省略
}
```
では，Daggerを使って上の`MyService`フィールドを差し替えられるようにします．

## Daggerで差し替えの仕組みづくり
`Dagger`でサーバ連携部分を差し替えられるように書き換えていきます．

まず，Applicationクラスを下のように書き換えて，
サーバー連携部分を外から差し込むようにします．

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

    @Inject MyService api;

    // 省略
```

次は，差し替えで使う以下2つのModuleを作っていきます．

1. サーバと連携する`RealAPIModule` (アプリ内で使う)
2. 決められた値をレスポンスとして返す`DummyAPIModule` (テストで使う)

サーバと連携する`RealAPIModule`は，今まで使っていたものを`@Provides`で返しているだけです．

```java:RealAPIModule.java
@Module(injects = MyApplication.class, library = true)
public class RealAPIModule {
    @Provides @Singleton
    public MyService provideMyService() {
        return new RestAdapter.Builder()
                .setEndpoint(ServerURL.END_POINT)
                .build()
                .create(MyService.class);
    }
}
```

次は，設定した値を返す`DummyAPIModule`です．

まず，指定した値を返す`MockClient`クラスの作成を行います．
ここでは，コンストラクタで受け取った文字列をレスポンスとして返すようしています．

```java:MockClient.java
public class MockClient implements Client{
    private static final int HTTP_OK_STATUS = 200;
    private final String RESPONSE_JSON;

    public MockClient(String responce_json) { this.RESPONSE_JSON = responce_json; }

    @Override
    public Response execute(Request request) throws IOException {
        return createResponseWithCodeAndJson(HTTP_OK_STATUS, RESPONSE_JSON);
    }

    private Response createResponseWithCodeAndJson(int responseCode, String json) {
        return new Response("",responseCode, "nothing", Collections.EMPTY_LIST,
                new TypedByteArray("application/json", json.getBytes()));
    }
}
```

`DummyAPIModule`はこうなります．
クライアントに`MockClient`を使うことで，「レスポンスをコントロール」しています．

```java:DummyAPIModule.java
public class DummyAPIModule {
    public String mock_response;

    public DummyAPIModule(String mock_response) { this.mock_response = mock_response; }

    @Provides
    @Singleton
    public MyService provideMyService() {
        return new RestAdapter
                .Builder()
                .setEndpoint(ServerURL.END_POINT)
                .setClient(new MockClient(mock_response))  // set mock client !!
                .build()
                .create(MyService.class);
    }

}
```

あとは，アプリとテストでModuleを切り替えるようににして完成です．

##アプリとテストでサーバ連携部分を差し替え
方針はこうなります．

- 基本的にはサーバにアクセスする`RealAPIModule`を使う
- テストの時だけ`DummyAPIModule`に切り替える．


まず，自作アプリケーションクラス `MyApplication.java`を次のように書き換えます．

```java:MyApplication.java
public class MyApplication extends Application {
    private ObjectGraph objectGraph = null;
    @Inject MyService api;

    @Override
    public void onCreate() {
        super.onCreate();
        if(objectGraph == null) {
            List modules = Collections.singletonList(new RealAPIModule());
            objectGraph = ObjectGraph.create(modules.toArray());
        }
    }

    // used to replace ObjectGraph for test
    public void setObjectGraph(ObjectGraph graph) { this.objectGraph = graph; }

    public ObjectGraph getObjectGraph() { return objectGraph; }

    public MyService api() { return api; }
}
```

次にベースアクティビティで，ModuleをInjectするように変えます．
これで「基本的にはサーバにアクセスする」ようになってます．

```java:BaseActivity
public class BaseActivity extends AppCompatActivity{

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ((MyApplication)getApplication())
                .getObjectGraph()
                .inject(getApplication());
    }
    
    // 省略
```

テストでは，次のようにして
「テストの時だけ`DummyAPIModule`に切り替える」ようにします．

```java:ImageListFragmentTest.java

@Rule
public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(
        MainActivity.class,
        true,     // initialTouchMode
        false);   // launchActivity. False so we can customize the intent per test method

private void setupMockServer(String response) {
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    MyApplication app =
            (MyApplication) instrumentation.getTargetContext().getApplicationContext();

    // setup objectGraph to inject Mock API
    List modules = Collections.singletonList(new DummyAPIModule(response));
    ObjectGraph graph = ObjectGraph.create(modules.toArray());
    app.setObjectGraph(graph);
    app.getObjectGraph().inject(app);
}

@Test
public void testEmptyView_show() {
    String empty_response = 
        "{\"photos\":{\"page\":1,\"pages\":25,\"perpage\":20,\"total\":500,\"photo\":[],\"stat\":\"ok\"}";
    setupMockServer(empty_response);

    // start Activity manually
    MainActivity activity = activityRule.launchActivity(new Intent());

    // check if empty view is visible
    // 省略
```
`ActivityTestRule`のおかげで，Activityは自動でスタートしません．
そのため，`DummyModule`を差し込んでから，`launchActivity`を呼ぶことで
差し替えが実現できます．

## 終わりに
もっと簡単な方法がありそうですが，Daggerを使ってみたかったのでやってみました．
合わせてDIに関していろいろ調べたので，
いいなと思ったURLをいくつか参考URLに貼ります．

ありがとうございました．

##参考URL
[Dagger (公式 website)](http://square.github.io/dagger/)
[Square Island : Dagger 2 + Espresso 2 + Mockito](http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html)
[Tasting Dagger 2 on Android | Fernando Cejas](http://fernandocejas.com/2015/04/11/tasting-dagger-2-on-android/)