LoginSignup
5
3

MockMvcではerror/404.htmlやerror.htmlをテスト不可能。その理由と代替策

Last updated at Posted at 2024-05-01

環境

  • Java 21
  • Spring Boot 3.2
  • Thymeleaf 3.1

やりたいこと(でも出来なかった)

MockMvcを利用したコントローラーの単体テストで、以下のテストを試みました。

  • コントローラーメソッド内で想定外の例外(=ExceptionHandlerを作っていない例外)がスローされたら、error.htmlの内容がレスポンスされる
  • 存在しないURLを指定したら、error/404.htmlの内容がレスポンスされる

具体的なテストコードはこちらです。

MockMvcを使ったテストコード
@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: 想定外の例外が発生しました。
...
404エラーのテスト結果
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によって処理されます。
BasicErrorControllerErrorMvcAutoConfigurationでBean定義されています。

ErrorMvcAutoConfiguration.java(一部引用)
@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());
	}
    ...
}
  1. このErrorMvcAutoConfigurationには@ConditionalOnWebApplication(type = Type.SERVLET)が付加されている
  2. MockMvcを使っている場合はこの条件に合致しない(デバッガで追って確かめました)
  3. ErrorMvcAutoConfiguration自体が無効化される
  4. BasicErrorControllerもBean定義されない
  5. よって、error.htmlなどが使われない

ちなみに、テストクラスに付いている@SpringBootTestwebEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORTを追加してもダメでした。これについては、ソースを追っても理由が分かりませんでした・・・。

代替策

ということで、MockMvcを使ったテストでは無理そうです。なので、他の手段を使うしかありません。

  1. テストコードを書くのは諦めて、ブラウザから手動でテストする。
  2. SeleniumやPlaywrightを使ってテストする。
  3. TestRestTemplateを使ってテストする。

最後の方法のコードはこんな感じです。

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))してください。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3