SpringBootで独自例外を作ってControllerAdviceでハンドリングする

概要

  • 独自RuntimeExceptionを作る
  • 独自例外はControllerAdviceで横断的にハンドリングする
  • テストを書く

前提

  • Kotlin
  • RestControllerを想定
    • ゆえにRestControllerAdviceを使う
  • テストはSpock

独自例外の作成

  • RuntimeExceptionを作成
  • コンストラクタ引数はnullを許可したくなかったので、superのコンストラクタにそれぞれ渡している
class MyException : RuntimeException {
    constructor(message: String, cause: Throwable) : super(message, cause)
    constructor(message: String) : super(message)
    constructor(cause: Throwable) : super(cause)
    constructor() : super()
}

ハンドラークラスの作成

  • クラスに @RestControllerAdvice を付与
  • ハンドルするメソッドに @ExceptionHandler を付与
  • ハンドルするメソッドに @ResponseStatus を付与
    • 指定したHTTPステータスがレスポンスに設定される
    • この例だとINTERNAL_SERVER_ERROR(500)
  • ハンドルするメソッドは対象の例外を引数で受け取る
    • 返り値の型がレスポンスボディに設定される
    • デフォルトだとRestControllerならJSONになると思う
@RestControllerAdvice
class MyExceptionHandler {
    @ExceptionHandler(MyException::class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    fun handleMyException(e: MyException): Map<String, String> = mapOf("message" to "Error!!")
}

テストの作成

  • クラスに @SpringBootTest@AutoConfigureMockMvc を付与
    • MockMvcをAutowiredしてテストする
  • スタブコントローラを作成する
    • CotrollerAdviceはコントローラに横断的に適用されるので、個別のコントローラに対しては(ユニット)テストしない
    • 常にテスト対象の例外をthrowするようなダミーのエンドポイントを用意する
  • テストクラスに @ActiveProfiles を、スタブに @Profile を付与する
    • 必須ではない
    • スタブのエンドポイントが重複した場合を避けるため
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("myExceptionHandlerSpec")
class MyExceptionHandlerSpec extends Specification {
    @Autowired
    MockMvc mockMvc

    def 'MyExceptionが発生した場合はHTTP:500になる'() {
        expect:
        mockMvc.perform(MockMvcRequestBuilders.get('/mock'))
                .andExpect(MockMvcResultMatchers.status().is(HttpStatus.INTERNAL_SERVER_ERROR.value()))
                .andExpect(MockMvcResultMatchers.jsonPath("\$.message").value("Error!!"))
    }
}

// ControllerAdviceをテストするためのStub
@RestController
@RequestMapping("/mock")
@Profile("exceptionHandlerSpec")
class StubController {
    @GetMapping
    def test() {
        throw new MyException()
    }
}

参考