読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

MockWebServerRuleを使ってOkHttpClientのリクエストをmockする

Android Mockito OkHttp

Mockito1.9.5+OkHttp2.1.0でリクエストをmockするを書いた後、mockwebserverを使った場合についても考えた方がいいかもなーと思って試してみた。結果的にはmockwebserver使った方がいいなーという感想。OkHttpClientを直接mockしたかったのは、リクエストURLに関係なくOkHttpClientの振る舞いを変更したかったから。mockwebserverの場合はmockwebserverが発行するURLへリクエストしなければならず不便かなと思ったんだけど、APIクライアントにせよ何にせよURLはinjectableであるべきなんじゃないかなーと思い直し、injectableだったらmockwebserverが発行するURLをinjectすればいいんだから問題ないよねとかんがえるようになった。この辺の詳細については後で書く。

mockwebserverを使った例について解説しますが、↓を直接見てもらった方が早いかもしれません。

sys1yagi/OkHttpMockTest · GitHub

設定

環境としては以下となる。基本的にokhttp, mockito, mockwebserverだけでいけるけどサンプルコードの為に必要なものと、より実際的な構成にするために色々ライブラリを使っている。それぞれのライブラリを使ったコードの詳細についてはあんまり解説しない。

  • dagger 1.2.2
  • gson 2.3.0
  • rxandroid 0.23.0
  • okhttp 2.1.0
  • mockwebserver 2.1.0
  • mockito 1.9.5
  • junit 4.11
  • robolectric.2.3
  • commons-io 2.4

build.gradleのdependenciesは以下の様になる。

apply plugin: 'robolectric'
//...
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'com.squareup.dagger:dagger:1.2.2'
    provided 'com.squareup.dagger:dagger-compiler:1.2.2'
    compile 'io.reactivex:rxandroid:0.23.0'
    compile 'com.squareup.okhttp:okhttp:2.1.0'
    compile 'com.google.code.gson:gson:2.3.1'

    androidTestCompile 'commons-io:commons-io:2.4'
    androidTestCompile 'com.squareup.okhttp:mockwebserver:2.1.0'
    androidTestCompile('org.mockito:mockito-core:1.9.5') {
        exclude group: 'org.hamcrest'
        exclude module: 'objenesis'
    }
    androidTestCompile 'junit:junit:4.11'
    androidTestCompile 'org.robolectric:robolectric:2.3'
}

構成

テストコードを読むために必要なクラスを解説します。ザックリ言うと「商品を表すモデルがあり、それを読み込むObservableがあり、Observableが利用する各種クラスを提供するdaggerのModuleがある」そしてそれをテストする感じです。ちょこちょこ中略するので詳細はgithubの方見てください。

Item.java

商品を表すクラス。

public class Item {
  int id;
  String name;
  String description;
  int price;
  //..
}

ItemObservable.java

指定したIDのItemをエンドポイントから取得するObservable。@Singleton@Injectはdaggerのためのもの。injectされたOkHttpClientを使ってリクエストをし、取得したjsonをgsonでItemに変換してonNext()する。buildPath(int id)は後ほどMockitoでspyして上書きする為に定義している。

@Singleton
public class ItemObservable {
  @Inject
  OkHttpClient okHttpClient;
  @Inject
  Gson gson;
  @Inject
  public ItemObservable() {
  }
  public String buildPath(int id) {
    return "https://dummy.api.endpoint/" + id;
  }
  public Observable<Item> fromId(final int id) {
    return Observable.create(new Observable.OnSubscribe<Item>() {
      @Override
      public void call(Subscriber<? super Item> subscriber) {
        String url = buildPath(id);
        Request request = new Request.Builder().url(url).build();
        try {
          Response response = okHttpClient.newCall(request).execute();
          if (response.isSuccessful()) {
            String json = response.body().string();
            subscriber.onNext(gson.fromJson(json, Item.class));
            subscriber.onCompleted();
          } else {
            //TODO
            //I think better that define the OkHttpClientException.
            subscriber.onError(new Exception(response.message()));
            subscriber.onCompleted();
          }
        } catch (IOException e) {
          subscriber.onError(e);
          subscriber.onCompleted();
        }
      }
    });
  }
}

AppModule.java

daggerでinjectする方々を定義している。

@Module(injects = MainActivity.class)
public class AppModule {
  @Provides
  @Singleton
  public OkHttpClient provideOkHttpClient() {
    return new OkHttpClient();
  }
  @Provides
  @Singleton
  public Gson provideGson() {
    return new Gson();
  }
}

実際ItemObservableを使っている所

MainActivityで利用例を書いてますが実際はエンドポイントが不正なので動かない。利用側の雰囲気だけ見てもらえると。AndroidObservableのbindActivity()を使ってライフサイクルに合わせてコールバックの制御をしてもらっている。また、ItemObservable自身は勝手に非同期なObservableを返す事はせず利用側でsubscribeOn()で設定している。

@Inject
ItemObservable itemObservable;
@Override
public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  ((Application) getApplication()).inject(this);

  //ItemObservable doesn't work. It is example of code.
  AndroidObservable.bindActivity(this, itemObservable.fromId(10)
      .subscribeOn(Schedulers.newThread())).subscribe(
      new Action1<Item>() {
        @Override
        public void call(Item item) {
          showItem(item);
        }
      },
      new Action1<Throwable>() {
        @Override
        public void call(Throwable throwable) {
          //error
        }
      });
}

MockWebServerRuleを使ったテストコード

ItemObservableのテストコードは以下となる。MockWebServerRuleを使うとOkHttpClientはmockしなくてよくなる。コード中に解説を書いた。非常に簡単にmockできる!!

@RunWith(RobolectricTestRunner.class)
public class ItemObservableTest {
  @Module(injects = ItemObservableTest.class,
      includes = AppModule.class,
      overrides = true)
  class TestModule {
  }
  @Rule
  public MockWebServerRule server = new MockWebServerRule();
  @Inject
  ItemObservable itemObservable;
  @Before
  public void setUp() throws Exception {
    ObjectGraph graph = ObjectGraph.create(new TestModule());
    graph.inject(this);
  }
  @Test
  public void testFromId() throws Exception {
    //assetsに置いたファイルを読み込む
    File file = new File("src/androidTest/assets/item.json");
    String json = FileUtils.readFileToString(file);

    //レスポンスのモックを作り、serverにセットする。
    MockResponse response = new MockResponse()
        .setResponseCode(200)
        .setBody(json);
    server.enqueue(response);

    //ItemObservableをspyして、MockWebServerRuleが発行するURLを返す様にする
    itemObservable = spy(itemObservable);
    when(itemObservable.buildPath(anyInt())).thenReturn(server.getUrl("/").toString());

    //toBlocking()してsingle()するとコールバック無しでObservableから1件の結果を取り出せる
    Item item = itemObservable.fromId(1).toBlocking().single();
    assertThat(item, notNullValue());
    assertThat(item.getId(), is(10));
    assertThat(item.getName(), is("tomato"));
    assertThat(item.getDescription(), is("It is super sweet tomato!"));
    assertThat(item.getPrice(), is(98));
  }
}

mockwebserverが発行するURLをinjectする話

冒頭でも書いた通り、mockwebserverを使う場合mockwebserverが発行するURLにテスト対象がアクセスしなければならない。例えば以下の様な実装だとmockwebserverが発行するURLをセットできない。

public class ItemApiClient {
  public void request(int id){
    String url = "https://dummy.api.endpoint/" + id;
    Request request = new Request.Builder().url(url).build();
    //..
  }
}

なのでどんなURLが来ても好きなレスポンスを返す為にMockito1.9.5+OkHttp2.1.0でリクエストをmockするを考えたのだけど、そもそも↑の構造ってよくないんじゃー?これだとURLがパラメータやステータスによって構築された時の結果を簡単にテストできないし。以下の構造にしておくとテストも書けていいよなーと、でこれだったらmockwebserverでいいじゃんという事になる。tastableかどうかって大事だな~と思った。

public class ItemApiClient {

  String buildUrl(int id){
    return "https://dummy.api.endpoint/" + id;
  }
  public void request(int id){
    String url = buildUrl(id);
    Request request = new Request.Builder().url(url).build();
    //..
  }
}

まとめ

  • URLはinjectableがいいね
  • APIに対するクライアントならretrofitの方がよりテスト書きやすそう
  • OkHttpClientをmockして頑張るよりmockwebserver使った方が楽