Springで外部APIをリクエストする場合のテスト

外部APIをリクエストしている機能のテストをしたい場合、Springでは、MockRestServiceServerを使います。

MockRestServiceServerを使うと、RestTemplateがリクエストしたURL等の条件に応じて、期待したレスポンスを返すようにテストを構成できるようになります。また必要に応じて、モックされたリクエストが、正しく順番通りに使用されたかどうかも検証できます。

なお、今回のサンプルコードは以下にあるので、こちらも参考に。https://github.com/yo1000/example.MockRestServiceServer/tree/master/MockRestServiceServer-client

要件

  • Java 1.8.0_121
  • Maven 3.5.3
  • Kotlin 1.2.41
  • Spring Boot 1.5.12.RELEASE
$ ./mvnw -v
Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-25T04:49:05+09:00)
Maven home: ~/.m2/wrapper/dists/apache-maven-3.5.3-bin/2c22a6s60afpuloj4v181qvild/apache-maven-3.5.3
Java version: 1.8.0_121, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "10.13.3", arch: "x86_64", family: "mac"

$ cat MockRestServiceServer-client/pom.xml | grep kotlin.version | grep -v '\$'
		<kotlin.version>1.2.41</kotlin.version>

$ cat MockRestServiceServer-client/pom.xml | grep spring-boot-starter-parent -A1 | grep version
		<version>1.5.12.RELEASE</version>

使い方

早速、サンプルを見ていきます。

サンプル

まずテスト対象になる、外部APIをリクエストするクラスから。

@Configuration
class ExampleConfiguration {
    @Bean
    fun restTemplate(): RestTemplate = RestTemplate()
}

@RestController
@RequestMapping("/client")
class ExampleClientController(
        val restTemplate: RestTemplate
) {
    @GetMapping
    fun get(): Any {
        val mapA = restTemplate.getForObject("http://localhost:8081/server/a", Map::class.java)
        val mapB = restTemplate.getForObject("http://localhost:8081/server/b", Map::class.java)

        return listOf(mapA, mapB)
    }
}

次に外部APIリクエストをモックするテストです。

@RunWith(SpringJUnit4ClassRunner::class)
@SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.RANDOM_PORT)
class MockRestServiceServerTest {
    @Autowired
    lateinit var context: WebApplicationContext
    @Autowired
    lateinit var restTemplate: RestTemplate

    @Test
    fun test() {
        val mockMvc = MockMvcBuilders
                .webAppContextSetup(context)
                .build()

        MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(false).build().let {
            it.expect(MockRestRequestMatchers.requestTo(
                    "http://localhost:8081/server/a"
            )).andExpect(
                    MockRestRequestMatchers.method(HttpMethod.GET)
            ).andRespond(MockRestResponseCreators.withSuccess("""
                {
                  "example": "A"
                }
                """.trimIndent(), MediaType.APPLICATION_JSON_UTF8
            ))

            it.expect(MockRestRequestMatchers.requestTo(
                    "http://localhost:8081/server/b"
            )).andExpect(
                    MockRestRequestMatchers.method(HttpMethod.GET)
            ).andRespond(MockRestResponseCreators.withSuccess("""
                {
                  "example": "B"
                }
                """.trimIndent(), MediaType.APPLICATION_JSON_UTF8
            ))
        }

        mockMvc.perform(MockMvcRequestBuilders.get("/client"))
                .andDo(MockMvcResultHandlers.print())
                .andExpect(MockMvcResultMatchers.status().isOk)
                .andExpect(MockMvcResultMatchers.jsonPath("$").isArray)
                .andExpect(MockMvcResultMatchers.jsonPath("$").value(Matchers.hasSize<Int>(2)))
                .andExpect(MockMvcResultMatchers.jsonPath("$[0].example").value("A"))
                .andExpect(MockMvcResultMatchers.jsonPath("$[1].example").value("B"))
    }
}

解説

モック設定のために使われる、主なメソッドは以下です。

bindTo

bindToメソッドを使って、DIコンテナに登録済みのRestTemplateインスタンスを、MockRestServiceServerに渡してやることで、渡されたRestTemplateを使用したリクエストがモックされるようになります。

なお、**DIコンテナに登録されるRestTemplateインスタンスは、テストクラス、およびテスト対象クラスともに、同じインスタンスを指している必要があります。**そのため、プロダクション構成でRestTemplateインスタンスにリクエストスコープ等を設定している場合は、テスト構成ではシングルトンスコープで動作するように設定しておく必要があります。

ignoreExpectOrder

ignoreExpectOrderメソッドでは、外部APIリクエストの呼び出し順序の検証を、無視するかどうかを設定します。デフォルトはfalseです。そのため、デフォルト設定のままテストする場合は、外部APIリクエストのモック定義順序は、実際に外部リクエストを行う順序と完全に一致させる必要があります。

expect, andExpect

expectメソッドでは、モックするリクエストのURLや、HTTPメソッド、ヘッダ、クエリパラメタ等、どのようなリクエストを受けたら、モックされたレスポンスを返すのか、という条件を定義します。

andRespond

andRespondメソッドでは、モックされたリクエストが、レスポンスボディ、HTTPステータスコード等、何をレスポンスするのかを定義します。

所感

以上のように、使い方に少々の癖はありますが、設定自体は簡単で、とても便利にテストに活かすことができました。

呼び出し順序や、呼び出されたかどうかの検証まで行ってくれるため、モックすべきリクエストは、すべてモックしなければならず、必然的にテストコードは肥大化しやすくなってしまうのですが、それでも外部APIリクエストを簡単にモックできるというのは、とてもありがたい機能です。

avatar

Written by yo1000 | YO!CHI KIKUCHI
Loves 🌱 Spring, 🦢 Pelikan fountain pen and 🦁 FINAL FANTASY VIII