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>
1
2
3
4
5
6
7
8
9
10
11
12
13

使い方

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

サンプル

まずテスト対象になる、外部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)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

次に外部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"))
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

解説

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

bindTo

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

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

ignoreExpectOrder

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

expect, andExpect

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

andRespond

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

所感

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

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