RestTemplateで外部のWeb APIにアクセスするクラスのテストを、MockRestServiceServerを使って書く

# やりたいこと
Springの`RestTemplate`で外部のWeb APIにアクセスするクラスのテストを書きたいです。しかしその場合、アクセス先のサーバーをどうやって用意するかが問題になります。

[WireMock](https://wiremock.org/)など、モックサーバーを用意するためのライブラリも存在します（個人的には使ったことないので詳しくは分かりません）。

今回はSpringで用意されている`MockRestServiceServer`を使ってみます。`MockRestServiceServer`は名前の通りモックサーバー機能を持つクラスです。`RestTemplate`と連携することができます。

# 環境
- JDK 17
- Spring Boot 3.1.2

> 将来のバージョンアップにより、内容が正しくなくなる可能性もあるので注意してください。

# サンプルコード
https://github.com/MasatoshiTada/mock-rest-service-server-sample

# Web APIの仕様
## GET /api/hello
ステータスコード200、レスポンスボディは下記

```json:レスポンスボディ
{"message": "hello"}
```

## POST /api/hello
リクエストボディは下記

```json:リクエストボディ
{"message": "hello"}
```

レスポンスはステータスコード200、ボディなし

# テスト対象
```java:HelloClient.java
@Component
public class HelloClient {

    private final RestTemplate restTemplate;

    public HelloClient(RestTemplateBuilder restTemplateBuilder, @Value("${hello-service.base-url}") String baseUrl) {
        this.restTemplate = restTemplateBuilder.rootUri(baseUrl).build();
    }

    public HelloResponse getHello() {
        ResponseEntity<HelloResponse> responseEntity = restTemplate.getForEntity("/api/hello", HelloResponse.class);
        if (responseEntity.getStatusCode().isError()) {
            throw new RuntimeException("error");
        }
        return responseEntity.getBody();
    }

    public String postHello(HelloRequest request) {
        ResponseEntity<Void> responseEntity = restTemplate.postForEntity("/api/hello", request, Void.class);
        if (responseEntity.getStatusCode().isError()) {
            throw new RuntimeException("error");
        }
        return "OK";
    }
}
```

```java:HelloRequest.java
public record HelloRequest(String message) {
}
```

```java:HelloResponse.java
public record HelloResponse(String message) {
}
```

# テストコード
テストクラスに`@RestClientTest`を付加します。これにより`MockRestServiceServer`がBean定義済み、かつ`RestTemplate`と連携済み（具体的には`MockRestServiceServer#bindTo(RestTemplate)`が実行済みの状態）になります。`components`要素に`HelloClient.class`を指定することで、`HelloClient`もBean定義済みになります。

`HelloClient`のコンストラクタの第2引数に`@Value("${hello-service.base-url}")`が付加されているので、`properties`要素で適当な値を指定します（本当に適当で大丈夫そうです）。

```java:HelloClientTest.java(一部)
@RestClientTest(
        components = HelloClient.class,
        properties = "hello-service.base-url=http://localhost"
)
class HelloClientTest {

    @Autowired
    HelloClient helloClient;

    @Autowired
    MockRestServiceServer server;
    ...
}
```

## GETリクエストに対するテスト
```java:HelloClientTest.java(一部)
    @Nested
    @DisplayName("getHello()")
    class GetHelloTest {
        @Test
        @DisplayName("GETリクエストを送信してサーバーから200が返ると、レスポンスボディをHelloResponseで受け取れる")
        void success() {
            // モックサーバーを設定する
            server.expect(requestTo("/api/hello"))  // このURLに
                    .andExpect(method(HttpMethod.GET))  // GETリクエストすると
                    .andRespond(withSuccess("""
                            {"message":"hello"}
                            """, MediaType.APPLICATION_JSON));  // 200 OKとこんなJSONを返すよう設定する
            // テスト実行＋アサーション
            HelloResponse actual = helloClient.getHello();
            assertEquals("hello", actual.message());
        }

        @Test
        @DisplayName("GETリクエストを送信してサーバーから500が返ると、RuntimeExceptionがスローされる")
        void error() {
            // モックサーバーを設定する
            server.expect(requestTo("/api/hello"))  // このURLに
                    .andExpect(method(HttpMethod.GET))  // GETリクエストすると
                    .andRespond(withServerError());  // 500が返る
            // テスト実行＋アサーション
            assertThrows(RuntimeException.class, () -> helloClient.getHello());
        }
    }
```

## POSTリクエストに対するテスト
```java:HelloClientTest.java(一部)
    @Nested
    @DisplayName("postHello()")
    class PostHelloTest {
        @Test
        @DisplayName("JSONをPOSTしてサーバーから200が返ると、'OK'が返る")
        void success() {
            server.expect(requestTo("/api/hello"))  // このURLに
                    .andExpect(method(HttpMethod.POST))  // POSTリクエストで
                    .andExpect(content().json("""
                            {"message":"hello"}
                            """))  // こんなJSONを送信すると
                    .andRespond(withStatus(HttpStatus.OK));  // 200が返るよう設定する
            // テスト実行＋アサーション
            String actual = helloClient.postHello(new HelloRequest("hello"));
            assertEquals("OK", actual);
        }

        @Test
        @DisplayName("JSONをPOSTしてサーバーから500が返ると、RuntimeExceptionがスローされる")
        void error() {
            server.expect(requestTo("/api/hello"))  // このURLに
                    .andExpect(method(HttpMethod.POST))  // POSTリクエストで
                    .andExpect(content().json("""
                            {"message":"hello"}
                            """))  // こんなJSONを送信すると
                    .andRespond(withServerError());  // 500が返るように設定する
            // テスト実行＋アサーション
            assertThrows(RuntimeException.class, () -> helloClient.postHello(new HelloRequest("hello")));
        }
    }
```

# 参考資料
- [`MockRestServiceServer`のJavadoc](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/test/web/client/MockRestServiceServer.html)
- [Spring Boot公式ドキュメント](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.testing.spring-boot-applications.autoconfigured-rest-client)
- [SpringのRestTemplateを使うコンポーネントのJUnitテストはこう書く！！](https://qiita.com/kazuki43zoo/items/fa9fea1c813f76080fe7)
    - ちょっとバージョンが古いので異なる部分がありますが（`@RestClientTest`が無いなど）、`MockRestServiceServer`の仕組みなどは変わらないはずです