怠惰系エンジニアのメモ帳

勉強した内容をメモしていきます。解説ブログではないので悪しからず。

【SpringWebFlux】エラーハンドラ(WebExceptionHandler)のテストを行う

SpringWebFluxでのエラーハンドリングは、WebExceptionHandler を継承してハンドラを作成する。

以下のようなエラーハンドラが定義されていると仮定。

@Component
public class GlobalErrorHandler implements WebExceptionHandler {

    @Override
    public Mono<Void> handle(final ServerWebExchange serverWebExchange, final Throwable throwable) {
        return ErrorHandleProvider.of(throwable) // ①
                .createResponse() // ②
                .flatMap(sr -> sr.writeTo(serverWebExchange, HandleContext.of(HandlerStrategies.withDefaults()))) // ③
                .flatMap(v -> Mono.empty()); // ④
    }
    ...
}

ざっと何をやっているか説明しておくと、

  • ①:ErrorHandleProvider は受け取った Throwable でハンドリング処理を提供するためのクラス。
    • ちなみに ErrorHandleProvider は自前で作成したクラス。(実装は割愛)
  • ②:クライアントに返却する Mono<ServerResponse> を作成。
    • この処理では、Httpステータスの設定と Throwable に設定されたメッセージをレスポンスボディに設定。
  • ③:ServerWebExchange に対して、Mono<ServerResponse> に設定されている情報を書き込む。
  • ④:handleメソッドの戻り値は Mono<Void> なので、Mono.empty() で空の Mono を作成。

となっています。

今回はこのエラーハンドラをテストする。
テストしたいのは

  1. あるエンドポイントに対して処理を依頼
  2. 依頼された処理でエラーが発生
  3. クライアントに対して、エラーに応じた情報が返る

というケースになります。
3ついては、例えば業務エラー(ApplicationException)がスローされた場合に

  • Httpステータス:500(Internal Server Error)
  • ボディ:ApplicationException に設定されている message

をレスポンスとして返却するといった具合。

準備

まず、エラーを発生させるエンドポイントの作成を行う。

@RunWith(SpringRunner.class)
public class GlobalErrorHandlerTest {

    private DispatcherHandler dispatcherHandler;

    @Before
    public void before() {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TestConfig.class);
        this.dispatcherHandler = new DispatcherHandler(ctx);
    }

    @EnableWebFlux
    @Configuration
    static class TestConfig {
        @Bean
        RouterFunction<ServerResponse> routerFunction() {
            return RouterFunctions.route(GET("/app-error"), request -> {
                        throw new ApplicationException("application exception");
                    });
        }
    }
}

TestConfigクラスでは、エンドポイントとなる RouterFunction をコンテナに登録しています。
GETメソッドで/app-errorにアクセスすると、ApplicationExceptionがスローされるように定義。

テストを書く

テストコード。

@Test
public void アプリケーションエラーがスローされた場合() throws Exception {
    // GETメソッドで /app-error にアクセスする
    MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/app-error").build());

    // WebHandlerを作成し、ディスパッチ。
    List<WebExceptionHandler> handlers = Collections.singletonList(new GlobalErrorHandler());
    WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, handlers);
    webHandler.handle(exchange).block();

    // チェック
    assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
    assertThat(exchange.getResponse().getBodyAsString().block()).isEqualTo("application exception");
}

まず最初に特定のURLにアクセスするためのリクエストと、そのリクエストを受け取る ServerWebExchange のモックを作成しています。 (どうも ServerWebExchange は、リクエスト〜レスポンスまでの情報を保持するオブジェクトのよう)
次に DispatcherHandler に対してハンドラ(WebExceptionHandler)を登録し、handle メソッドでハンドラの呼び出しを行います。 (WebHandler#handleメソッドは、DispatcherHandlerhandle メソッドを呼び出しているだけ)
ちなみに block() しないとsubscribeされないので注意。

ハンドラが処理した結果は、exchangeMockServerWebExchange)に書き込まれています。
書き込まれた値は MockServerWebExchangeAPIを経由して取得します。

ボディ部を特定のオブジェクトに変換したい場合は、 MockServerWebExchange#getBodyAsString() で取得した文字列からJacksonとか使って変換するしかなさそう?

また、WebClient からアクセスする場合を想定したテストケースは以下。

@Test
public void clientTest() {
    List<WebExceptionHandler> handlers = Collections.singletonList(new GlobalErrorHandler());
    WebHandler webHandler = new ExceptionHandlingWebHandler(this.dispatcherHandler, handlers);

    WebTestClient webTestClient = WebTestClient.bindToWebHandler(webHandler).build();
    WebTestClient.ResponseSpec spec = webTestClient.get().uri("/app-error").exchange();

    spec.expectStatus().is5xxServerError();
    spec.expectHeader().contentType(MediaType.APPLICATION_JSON);
    assertThat(new String(spec.expectBody().returnResult().getResponseBody())).isEqualTo("application exception");
}

WebTestClient.ResponseSpec に検証用のメソッドが用意されているのでいいですね。