MockWebServerRuleを使ってOkHttpClientのリクエストをmockする
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使った方が楽