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リクエストを簡単にモックできるというのは、とてもありがたい機能です。