環境
- Java 21
- Spring Boot 3.2
- Thymeleaf 3.1
やりたいこと(でも出来なかった)
MockMvcを利用したコントローラーの単体テストで、以下のテストを試みました。
- コントローラーメソッド内で想定外の例外(=ExceptionHandlerを作っていない例外)がスローされたら、error.htmlの内容がレスポンスされる
- 存在しないURLを指定したら、error/404.htmlの内容がレスポンスされる
具体的なテストコードはこちらです。
@SpringBootTest // webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT を指定しても結果は同じ
@AutoConfigureMockMvc
public class SampleControllerTest {
@MockBean
SampleService sampleService;
@Autowired
MockMvc mvc;
@Nested
@DisplayName("想定外の例外")
class ErrorTest {
@Test
@DisplayName("想定外の例外がスローされたら、エラー画面に遷移する")
void exception() throws Exception {
when(sampleService.doSomething()).thenThrow(new RuntimeException("想定外の例外が発生しました。"));
mvc.perform(get("/"))
.andExpect(status().isInternalServerError())
.andExpect(view().name("error"));
}
}
@Nested
@DisplayName("404エラー")
class NotFoundTest {
@Test
@DisplayName("存在しないURLにアクセスしたら、404エラー画面に遷移する")
void notFound() throws Exception {
mvc.perform(get("/xxx"))
.andExpect(status().isNotFound())
.andExpect(view().name("error/404"));
}
}
}
このテストを実行すると結果がNGとなりました。
jakarta.servlet.ServletException: Request processing failed: java.lang.RuntimeException: 想定外の例外が発生しました。
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1022)
...
Caused by: java.lang.RuntimeException: 想定外の例外が発生しました。
...
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 404
Error message = No static resource xxx.
Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
java.lang.AssertionError: No ModelAndView found
なぜNGになったのか?
error/404.htmlやerror.htmlはBasicErrorController
を通じてDefaultErrorViewResolver
によって処理されます。
BasicErrorController
はErrorMvcAutoConfiguration
でBean定義されています。
@AutoConfiguration(before = WebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET) // コレが原因
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
...
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().toList());
}
...
}
- この
ErrorMvcAutoConfiguration
には@ConditionalOnWebApplication(type = Type.SERVLET)
が付加されている -
MockMvc
を使っている場合はこの条件に合致しない(デバッガで追って確かめました) -
ErrorMvcAutoConfiguration
自体が無効化される -
BasicErrorController
もBean定義されない - よって、error.htmlなどが使われない
ちなみに、テストクラスに付いている@SpringBootTest
にwebEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
を追加してもダメでした。これについては、ソースを追っても理由が分かりませんでした・・・。
代替策
ということで、MockMvc
を使ったテストでは無理そうです。なので、他の手段を使うしかありません。
- テストコードを書くのは諦めて、ブラウザから手動でテストする。
- SeleniumやPlaywrightを使ってテストする。
-
TestRestTemplate
を使ってテストする。
最後の方法のコードはこんな感じです。
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SampleApplicationIntegrationTest {
@Autowired
TestRestTemplate restTemplate;
@MockBean
SampleService idolService;
@Nested
@DisplayName("想定外の例外")
class ErrorTest {
@Test
@DisplayName("想定外の例外がスローされたら、エラー画面に遷移する")
void exception() throws Exception {
when(sampleService.doSomething()).thenThrow(new RuntimeException("想定外の例外が発生しました。"));
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setAccept(List.of(MediaType.TEXT_HTML));
RequestEntity<Object> requestEntity = new RequestEntity<>(httpHeaders, HttpMethod.GET, URI.create("/"));
ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
assertAll(
() -> assertEquals(500, responseEntity.getStatusCode().value()),
() -> assertTrue(responseEntity.getBody().contains("エラーが発生しました。システム管理者に問い合わせてください。"))
);
}
}
@Nested
@DisplayName("404エラー")
class NotFoundTest {
@Test
@DisplayName("存在しないURLにアクセスしたら、404エラー画面に遷移する")
void notFound() throws Exception {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setAccept(List.of(MediaType.TEXT_HTML));
RequestEntity<Object> requestEntity = new RequestEntity<>(httpHeaders, HttpMethod.GET, URI.create("/xxx"));
ResponseEntity<String> responseEntity = restTemplate.exchange(requestEntity, String.class);
assertAll(
() -> assertEquals(404, responseEntity.getStatusCode().value()),
() -> assertTrue(responseEntity.getBody().contains("指定されたURLは存在しません。"))
);
}
}
}
これならテストはOKになりました。
追記2024-05-04
公式リファレンスにも書いてありました!
This means that, whilst you can test your MVC layer throws and handles exceptions as expected, you cannot directly test that a specific custom error page is rendered. If you need to test these lower-level concerns, you can start a fully running server as described in the next section.
日本語訳
これはつまり、MVCレイヤーが期待通りに例外をスローしたりハンドリングしたりするかをテストできると同時に、特定のカスタムエラーページが表示されるかを直接テストすることはできません。これらの低レイヤーな関心事をテストする必要があるならば、次節で解説されるようにサーバーを完全に起動(訳註:
@SpringBootTest(webEnvironment = RANDOM_PORT)
)してください。