Webfluxでの単体テスト（Junit）

# 概要

- SpringWebで書いていたテストのあれこれをwebfluxではどう書くのか気になったので調べてみました

# WebClient利用クラス

WebClient利用している外部APIを呼び出すクラスについてです。

## テスト対象のクラス

```java
@Component
public class JunitSampleClient {
    private final WebClient webClient;

    public JunitSampleClient(@Value("{baseUrl}") String baseUrl) {
        this.webClient = WebClient.builder()
                .baseUrl(baseUrl)
                .build();
    }

    public Mono<String> ping() {
        return webClient.get()
              .uri("/ping")
              .retrieve()
              .bodyToMono(String.class);
    }
}
```

このクラスをテストしようとした時に考えることは以下の２つかなと思います。

- WebClientをどうMock化するか？
- Mono（戻り値）をどう検証するか？

それぞれ見ていきます

## WebClientをどうMock化するか？

WebClientのMockをしようとすると、メソッドチェーン分のMockを準備してあげなければならず、大変。。。
なので「mockwebserver」を使用してモックサーバを立ち上げます

まずは必要なライブラリを追加します

```groovy:build.gradle
dependencies {
    ...
    testImplementation 'com.squareup.okhttp3:okhttp:4.9.1'
    testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.1'
    ...
}
```

モックサーバは`MockWebServer`を起動すればOK！

```java
// 起動
MockWebServer mockServer = new MockWebServer();
// 停止
mockServer.shutdown();
```

そして`enqueue`メソッドでAPIのレスポンスを定義してあげます。

```java
mockServer.enqueue(new MockResponse()
        .setBody("pong"));
```

モックサーバは、ランダムなポートで起動するためどのポートで起動したか確認するために`getPort()`メソッドが用意されています。

↓参考↓

https://www.baeldung.com/spring-mocking-webclient

## Mono（戻り値）をどう検証するか？

WebClientに限らず、`Mono`, `Flux`を使う場合に通じて言えることですが、
`Mono`の検証には`StepVerifier`を使います。

```java
StepVerifier.create(Mono.just("pong"))
        // 要素の検証(fluxの場合には、メソッドチェーンで複数回実行)
        .expectNext("pong")
        // 要素がもうないことを検証
        .expectComplete()
        // 検証実行
        .verify();
```

`block()`メソッドで、要素を取り出すことで非reactと同じようにテストをすることも可能ですが、
リアクティブならではの動作（例：delay）の検証はできないため
慣れるためにも`StepVerifier`を使う方が良いのではないかと思います。

注意点としては`verify()`を呼び出して初めて`expectXXX()`の内容が検証されるので、
すべてのテストで必ず`verify()`を呼び出すことです。

## テストクラス

上記を踏まえて実装したテストがこちら

```java
class JunitSampleClientTest {
    private JunitSampleClient junitSampleClient;

    public static MockWebServer mockServer;
    private static String baseUrl;

    @BeforeAll
    static void setUp() throws IOException {
        mockServer = new MockWebServer();
        mockServer.start();
        baseUrl = "http://localhost:" + mockServer.getPort();
    }

    @AfterAll
    static void tearDown() throws IOException {
        if (mockServer != null) {
            mockServer.shutdown();
        }
    }

    @BeforeEach
    void beforeEach() {
        junitSampleClient = new JunitSampleClient(baseUrl);
    }

    @Test
    void test() {
        mockServer.enqueue(new MockResponse()
                .setBody("pong"));
        Mono<String> response = junitSampleClient.ping();

        StepVerifier.create(response)
                // 要素の検証(fluxの場合には、メソッドチェーンで複数回実行)
                .expectNext("pong")
                // 要素がもうないことを検証
                .expectComplete()
                // 検証実行
                .verify();
    }
}
```

# Controller

## テスト対象のクラス

```java
@RestController
@RequiredArgsConstructor
public class JunitSampleController {
    private final JunitSampleClient junitSampleClient;

    @GetMapping("/junit/sample")
    public Mono<String> sample() {
        return junitSampleClient.ping();
    }
}
```

さきほどの`JunitSampleClient`を依存にもつControllerです。

spring-webの場合、Controllerのテストは`@MockMvcTest`を使用してテストを実行します。
spring-webfluxの場合はどうなのでしょう？

それを踏まえ、このクラスをテストしようとした時に考えることは以下の２つかなと思います。

- webflux版`@MockMvcTest`は？
- 依存コンポーネントをどうMock化するか？

それぞれ見ていきます。

## webflux版`@MockMvcTest`は？

結論から言うと`@WebFluxTest`を使用します。
`@WebFluxTest`を使用すると、`WebTestClient`という、

- ローカルで起動するportに対応したbaseUrlを設定してくれていて
- レスポンスの検証メソッドが用意されている

クラスがBeanに登録されるので、Autowiredで取得します。

```java
// 引数にテスト対象のクラスを設定する
@WebFluxTest(JunitSampleController.class)
class JunitSampleControllerTest {
    @Autowired
    private WebTestClient webClient;
}
```

テスト時はWebClientと同じ使い方でリクエストし、
`.expectXXX`というメソッドで各種検証をしてきます。

以下ではステータスとbodyを検証しています。

```java
webClient.get()
        .uri("/junit/sample")
        .exchange()
        // ↑↑↑　ここまではWebClientと同じ使い方
        // ↓↓↓　ここからは検証
        .expectStatus().isOk()
        .expectBody(String.class).isEqualTo("pong");
```

## 依存コンポーネントをどうMock化するか？

`@WebFluxTest`を使用すると、Controllerに必要なBeanのみにしぼってSpringBootが起動するため、
依存をMock化したければ`MockBean`で登録されているBeanをMock化してあげます。

```java
@MockBean
private JunitSampleClient junitSampleClient;
...
Mockito.when(junitSampleClient.ping())
        .thenReturn(Mono.just("pong"));
```

これは知っている人には当然なことかもしれませんが
実際動かしてみるまで「MockBeanでいいんだっけ？」と半信半疑だったので一応

## テストクラス

上記を踏まえて実装したテストがこちら

```java
@WebFluxTest(JunitSampleController.class)
class JunitSampleControllerTest {
    @Autowired
    private WebTestClient webClient;

    @MockBean
    private JunitSampleClient junitSampleClient;

    @Test
    void test() {
        Mockito.when(junitSampleClient.ping())
                .thenReturn(Mono.just("pong"));

        webClient.get()
                .uri("/junit/sample")
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("pong");
    }
}
```