概要
- SpringWebで書いていたテストのあれこれをwebfluxではどう書くのか気になったので調べてみました
WebClient利用クラス
WebClient利用している外部APIを呼び出すクラスについてです。
テスト対象のクラス
@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);
}
}
このクラスをテストしようとした時に考えることは以下の2つかなと思います。
- WebClientをどうMock化するか?
- Mono(戻り値)をどう検証するか?
それぞれ見ていきます
WebClientをどうMock化するか?
WebClientのMockをしようとすると、メソッドチェーン分のMockを準備してあげなければならず、大変。。。
なので「mockwebserver」を使用してモックサーバを立ち上げます
まずは必要なライブラリを追加します
dependencies {
...
testImplementation 'com.squareup.okhttp3:okhttp:4.9.1'
testImplementation 'com.squareup.okhttp3:mockwebserver:4.9.1'
...
}
モックサーバはMockWebServer
を起動すればOK!
// 起動
MockWebServer mockServer = new MockWebServer();
// 停止
mockServer.shutdown();
そしてenqueue
メソッドでAPIのレスポンスを定義してあげます。
mockServer.enqueue(new MockResponse()
.setBody("pong"));
モックサーバは、ランダムなポートで起動するためどのポートで起動したか確認するためにgetPort()
メソッドが用意されています。
↓参考↓
Mono(戻り値)をどう検証するか?
WebClientに限らず、Mono
, Flux
を使う場合に通じて言えることですが、
Mono
の検証にはStepVerifier
を使います。
StepVerifier.create(Mono.just("pong"))
// 要素の検証(fluxの場合には、メソッドチェーンで複数回実行)
.expectNext("pong")
// 要素がもうないことを検証
.expectComplete()
// 検証実行
.verify();
block()
メソッドで、要素を取り出すことで非reactと同じようにテストをすることも可能ですが、
リアクティブならではの動作(例:delay)の検証はできないため
慣れるためにもStepVerifier
を使う方が良いのではないかと思います。
注意点としてはverify()
を呼び出して初めてexpectXXX()
の内容が検証されるので、
すべてのテストで必ずverify()
を呼び出すことです。
テストクラス
上記を踏まえて実装したテストがこちら
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
テスト対象のクラス
@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の場合はどうなのでしょう?
それを踏まえ、このクラスをテストしようとした時に考えることは以下の2つかなと思います。
- webflux版
@MockMvcTest
は? - 依存コンポーネントをどうMock化するか?
それぞれ見ていきます。
webflux版@MockMvcTest
は?
結論から言うと@WebFluxTest
を使用します。
@WebFluxTest
を使用すると、WebTestClient
という、
- ローカルで起動するportに対応したbaseUrlを設定してくれていて
- レスポンスの検証メソッドが用意されている
クラスがBeanに登録されるので、Autowiredで取得します。
// 引数にテスト対象のクラスを設定する
@WebFluxTest(JunitSampleController.class)
class JunitSampleControllerTest {
@Autowired
private WebTestClient webClient;
}
テスト時はWebClientと同じ使い方でリクエストし、
.expectXXX
というメソッドで各種検証をしてきます。
以下ではステータスとbodyを検証しています。
webClient.get()
.uri("/junit/sample")
.exchange()
// ↑↑↑ ここまではWebClientと同じ使い方
// ↓↓↓ ここからは検証
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("pong");
依存コンポーネントをどうMock化するか?
@WebFluxTest
を使用すると、Controllerに必要なBeanのみにしぼってSpringBootが起動するため、
依存をMock化したければMockBean
で登録されているBeanをMock化してあげます。
@MockBean
private JunitSampleClient junitSampleClient;
...
Mockito.when(junitSampleClient.ping())
.thenReturn(Mono.just("pong"));
これは知っている人には当然なことかもしれませんが
実際動かしてみるまで「MockBeanでいいんだっけ?」と半信半疑だったので一応
テストクラス
上記を踏まえて実装したテストがこちら
@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");
}
}