Java
spring
testing
spring-boot

SpringのRestTemplateを使うコンポーネントのJUnitテストはこう書く!!

More than 1 year has passed since last update.

今回は、SpringのRestTemplateを使って外部のWeb API(REST API)にアクセスするようなコンポーネントに対するJUnitテストケースの書き方を紹介します。単体テストでは、Mockitoなどのモック化フレームワークを使って依存コンポーネントの振る舞いを変えることも多いと思いますが、今回は、Spring Testが提供しているMockRestServiceServerを使います。

動作検証環境

  • Spring Framework 4.3.0.BUILD-SNAPSHOT (2016/6/4時点)
  • Spring Boot 1.4.0.BUILD-SNAPSHOT (2016/6/4時点)

RestTemplateって何者!?

MockRestServiceServerが何者か説明する前に、RestTemplateが何者でどんな仕組みでWeb APIにアクセスしているか説明しておきましょう。
RestTemplateは、ざっくり言えばSpringが提供しているHTTPクライアント用のクラスです。少しだけ正確にいうと・・・・単純なHTTPクライアントより上位のレイヤに位置付けられ「JavaオブジェクトとHTTPボディの変換」「エラー時の例外変換」といったHTTPの通信処理には直接関係ないけど、あると便利な機能が盛り込まれています。そしてRestTemplateの大きな特徴としては、実際のHTTP通信処理をJava SEの標準クラス(java.net.HttpURLConnection)や3rdパーティ製のHTTPクライアントライブラリ(Apache Http Components, OkHttp, Nettyなど)を利用して行う仕組みになっているところでしょう。そして、通信レイヤに使うライブラリを意識する実装は不要です :thumbsup: (もちろんRestTemplateをコンフィギュレーションする時にはライブラリを意識する必要はありますけどね :sweat_smile: )

RestTemplateを中心としたSpringのHTTPクライアントのアーキテクチャは、だいたい以下のようなイメージです。

spring-resttemplate.png

No 説明
アプリケーション(ここでは@Repositoryクラス)がRestTemplateのメソッドを呼び出し、外部APIへのアクセスを依頼する
RestTemplateは、HttpMessageConverterを使用して、Javaオブジェクト(JavaBeanなど)をJSONなどに変換する。
RestTemplateは、HttpClientRequestFactory経由で外部APIへのHTTP通信を依頼する。
HttpClientRequestFactoryの実装クラス(デフォルトでは、Java SEのクラスを利用してHTTP通信を行うSimpleClientHttpRequestFactory)は、外部APIへHTTP通信を行う。
RestTemplateは、HttpMessageConverterを使用して、外部APIから応答されたJSONなどをJavaオブジェクト(JavaBeanなど)に変換する。

MockRestServiceServerって何者!?

で、MockRestServiceServerを使うと、RestTemplateの振る舞いを以下のように変更することができます。

spring-mockrequestserviceserver.png

HttpClientRequestFactoryの実装クラスが、テストケース側から指定したモックレスポンスを返却するクラス(MockClientHttpRequestFactory)に差し代わり、実際の外部APIを呼び出す代わりにモックレスポンスを応答してくれるわけです。こうすることで、HTTP通信部分を除いたRestTemplateとの連携部分(HttpMessageConverter、エラーハンドリング部品、共通処理を挟み込む部品など)を適用した状態でテストできます :thumbsup: この仕組みは、単体テスト観点のモックではなく、モジュール結合テスト観点時につかうモックという印象です(単体テスト観点でも使えますけどね)。

RestTemplateを使うコンポーネントを作る

前置きが長くなってしまいましたが、実際にテストをしてみましょう!!テストするためには・・・テスト対象のクラスを作る必要です :wink:

package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.web.client.RestOperations;

@Repository
public class TodoRepository {

    @Autowired
    RestOperations restOperations;

    public Todo findOne(String todoId) {
        return restOperations.getForObject("https://api.domain/todos/{todoId}", Todo.class, todoId);
    }

}

RestTemplateのBean定義も行う必要があります。

@Configuration
public class ExternalApiConfig {
    @Bean
    RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

RestTemplateを使うコンポーネントをテストする

本来なら、Spring MVC → @Controller@Service@RepositoryRestTemplate といった感じでコンポーネント結合した状態でテストする方がよいですが、ここでは、@RepositoryRestTemplateの部分だけ結合した状態でテストします。

@Autowired
RestTemplate restTemplate; // テスト対象のクラスで使用するRestTemplateと同じものをテストケースクラスにもインジェクションする

@Autowired
TodoRepository todoRepository; // テスト対象のBeanもインジェクションする

@Test
public void findOne() {

    // テスト対象のクラスで使用するRestTemplateにMockRestServiceServerを割り当てる
    // MockHttpClientRequestFactoryに差し替わる
    MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restTemplate).build();

    // テスト対象のクラスでアクセスリクエストに対して、モックレスポンスを設定する
    // ここでは、
    //  ・ HTTPレスポンスコード : 200 OK
    //  ・ HTTPレスポンスボディ : JSON
    // をモックレスポンスに設定している。
    mockServer.expect(requestTo("https://api.domain/todos/123"))
            .andRespond(withSuccess("{\"id\":\"123\", \"title\":\"タイトル\", \"finished\":false}", MediaType.APPLICATION_JSON_UTF8));

    // テスト実施
    Todo todo = todoRepository.findOne("123");

    // モックレスポンスを元に生成したTodoオブジェクトの検証する
    assertThat(todo.getId(), is("123"));
    assertThat(todo.getTitle(), is("タイトル"));
    assertThat(todo.isFinished(), is(false));

    // セットアップしたモックレスポンスが全て消費されたか検証する
    mockServer.verify();

}

どうでしょうか?いい感じですよね :laughing:
ここでは正常系(200 OK)のテストケースですが、クライアントエラー(4xx系)やサーバーエラー(5xx系)のテストも簡単にできちゃいます!!

MockRestServiceServerを紐解く

MockRestServiceServerの基本的な使い方は、↑で紹介した通りです。が、しかし、当然ながらいろいろな機能があるので、そちらも簡単に紹介しておきましょう。

複数リクエストに対応するには?

ひとつの処理で複数のAPI呼び出し(リクエスト)を行うこともあると思いますが、その場合はどうすればよいのでしょうか?テスト内で複数のリクエストに対応する場合は、テストシナリオにあわせてexpectメソッドを呼び出してモックレスポンスを用意するだけです。

// ...
mockServer.expect(requestTo("https://api.domain/todos/123"))
            .andRespond(withSuccess("{\"id\":\"123\", \"title\":\"タイトル\", \"finished\":false}", MediaType.APPLICATION_JSON_UTF8));
mockServer.expect(requestTo("https://api.domain/todos/124"))
            .andRespond(withSuccess("{\"id\":\"124\", \"title\":\"タイトル\", \"finished\":true}", MediaType.APPLICATION_JSON_UTF8));
// ...

Spring 4.3 テスト関連の主な変更点」で紹介しましたが、Spring 4.3から、指定したモックレスポンスを返却する回数を指定できるようになります。また、デフォルトではexpectを呼び出した順番でモックレスポンスが消費されますが、順番を無視するオプションも用意されます。

リクエストを特定する方法はURLだけ?

もちろんURL以外の値をつかってリクエストを特定できます。

  • HTTPメソッド
  • リクエストヘッダ
  • リクエストボディ

リクエストの特定は、expectandExpectメソッドをつかって絞り込みます。これらのメソッドにはRequestMatcherのオブジェクトを指定するのですが、Built-inで様々なRequestMatcherが用意されています。Built-inで用意されているRequestMatcherについては、MockRestRequestMatchersのソースコードみてください。JSONPathやXPathにも対応しています :thumbsup:

モックレスポンスのバリエーションは?

サンプルではHTTPレスポンスに「200 OK」、HTTPレスポンスボディに「JSON形式の文字列」を指定していますが、どのようなバリエーションがあるのでしょうか?モックレスポンスは、andRespondメソッドをつかって指定します。andRespondメソッドにはResponseCreatorのオブジェクトを指定するのですが、Built-inでいくつかのResponseCreatorが用意されています。Built-inで用意されているResponseCreatorについては、MockRestResponseCreatorsのソースコードみてください。

まとめ

RestTemplateを使うアプリのテストを行う上で、MockRestServiceServerは必須といっても過言ではない気がします。もちろんMockitoなどのフレームワークを利用する方法もありますが、個人的にはMockRestServiceServerを使うことを強くお勧めします :blush:

参考サイト