パルカワ2

最近はFlutterをやっています

Retrofitでテストするときは、MockClientを作る

RestAdapter.BuildersetClient()できるので、テスト時はそこにテスト用のClientをセットしてあげる。そのテスト用のClientをMockClientとしました。

本当は、Clientをわざわざ作るのがめんどくさいなと思って作らない方針で行こうとしたんですが、基盤チームの人にsetClientする方法があるでと改めて指摘されたあと考えなおして、Clientを作るのはだるいけど作ったほうが後々楽でわかりやすいなと思えてきたので作りました。

流れとしては、以下のような感じを想定しています。

  • @BeforeでMockClientをセットする(正確にはRestAdapterをセットする)
  • @Testで なにをどうモックするか指定する
  • @Afterで指定したモックをクリアする
// MockClientTest.java
public class MockClientTest {
    MockClient mMockClient = new MockClient();
    @Before
    public void setUp() throws Exception {
        // RestAdapterを作ってセット
        ApiClient.getInstance().setRestAdapter(RestAdapterFactory.newMocking(mMockClient));
    }

    @After
    public void tearDown() throws Exception {
        // 次のテストで引き継がないようにclear
        mMockClient.clear();
    }

    @Test
    public void testMock() throws Exception {
        // ここでAPIのbodyはなにを返すか指定する
        mMockClient.mock("/v1/reviews/100.json").to(200, "reviews/ok.json");
        Review review = ApiClient.getInstance().create(ReviewsService.class).fetch(100);

        assertThat(review.getStar()).isEqualTo(5);
    }

    @Test
    public void testMock_route() throws Exception {
        MockClient.Route route = mMockClient.mock("/v1/reviews/100.json").to(200, "reviews/ok.json");
        ApiClient.getInstance().create(ReviewsService.class).fetch(100);

        assertThat(route.called()).isEqualTo(true); // mockは呼ばれたか?
        assertThat(route.times()).isEqualTo(1); // 呼ばれた回数
    }

    @Test
    public void testMock_regex() throws Exception {
        // 正規表現でも可能
        MockClient.Route route = mMockClient.mock("/v1/reviews/\\d.json").to(200, "reviews/ok.json");
        ApiClient.getInstance().create(ReviewsService.class).fetch(100);

        assertThat(route.called()).isEqualTo(true);
    }
}
// MockClient.java
public class MockClient implements Client {
    List<Route> mRoutes = new ArrayList<>();
    Route notFoundRoute = new Route("404").to(404, "response/not_found.json");

    @Override
    public Response execute(Request request) throws IOException {
        for (Route route : mRoutes) {
            Response response = route.match(request.getUrl());
            if (response == null) {
                continue;
            }
            return response;
        }

        return notFoundRoute.newResponse(request.getUrl());
    }

    public Route mock(String urlPath) {
        Route route = new Route(urlPath);
        mRoutes.add(route);

        return route;
    }

    public void clear() {
        mRoutes = new ArrayList<>();
    }

    public class Route {
        Pattern mUrlPathRegex;
        int mStatus;
        int mTimes;
        String mJsonPath;

        Route(String urlPath) {
            mUrlPathRegex = Pattern.compile(urlPath);
        }

        public Route to(int status, String jsonPath) {
            mStatus = status;
            mJsonPath = jsonPath;

            return this;
        }

        public int times() {
            return mTimes;
        }

        public boolean called() {
            return mTimes >= 1;
        }

        private Response match(String url) throws IOException {
            Matcher matcher = mUrlPathRegex.matcher(url);
            if (!matcher.find()) {
                return null;
            }

            return newResponse(url);
        }

        private String getContentType() {
            return "application/json";
        }

        private Response newResponse(String url) throws IOException {
            // https://github.com/gfx/Android-Helium/blob/master/app/src/test/java/com/github/gfx/helium/TestUtils.java#L22
            byte[] body = TestUtils.getAssetFileInBytes(mJsonPath);

            mTimes++;
            return new Response(
                    url,
                    mStatus,
                    "mocked-response",
                    new ArrayList<Header>(),
                    new TypedByteArray(getContentType(), body)
            );
        }
    }
}

テストが並列で走ることとかはまだ考えてないです。
Clientを作るのめんどいと思ったけど、案外すぐ出来たので、そういうの良くないなって思いました。