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
に設定されたメッセージをレスポンスボディに設定。
- この処理では、Httpステータスの設定と
- ③:
ServerWebExchange
に対して、Mono<ServerResponse>
に設定されている情報を書き込む。 - ④:
handle
メソッドの戻り値はMono<Void>
なので、Mono.empty()
で空のMono
を作成。
となっています。
今回はこのエラーハンドラをテストする。
テストしたいのは
- あるエンドポイントに対して処理を依頼
- 依頼された処理でエラーが発生
- クライアントに対して、エラーに応じた情報が返る
というケースになります。
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
メソッドは、DispatcherHandler
の handle
メソッドを呼び出しているだけ)
ちなみに block()
しないとsubscribeされないので注意。
ハンドラが処理した結果は、exchange
(MockServerWebExchange
)に書き込まれています。
書き込まれた値は MockServerWebExchange
のAPIを経由して取得します。
ボディ部を特定のオブジェクトに変換したい場合は、
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
に検証用のメソッドが用意されているのでいいですね。