How does MockMvcTester handle a controller exception that no @ExceptionHandler resolves, and how does that differ from classic MockMvc?
answer
- Classic MockMvc rethrows unresolved exception out of perform
- MockMvcTester captures it in the result
- hasFailed() / doesNotHaveFailed() / failure()
- Resolved -> normal response (not a failure)
- failure() returns AssertJ ThrowableAssert
basics
~20 sMockMvcTester captures an unresolved handler exception in the result instead of throwing it out of the call. You assert on it with .hasFailed() and .failure(). Classic MockMvc rethrows the exception from perform(...), so you'd need assertThatThrownBy or expectedException.
solid answer
~40 sIn classic MockMvc, if a controller throws and no HandlerExceptionResolver/@ExceptionHandler resolves it, the exception propagates out of mockMvc.perform(...) — the test line itself throws, so you assert with try/catch or assertThatThrownBy. MockMvcTester instead captures that unresolved exception in the MvcTestResult. assertThat(mvc.get()...).hasFailed() asserts the exchange failed, and .failure() returns an AssertJ ThrowableAssert so you can check type and message: .failure().isInstanceOf(IllegalStateException.class).hasMessageContaining("..."). Conversely .doesNotHaveFailed() asserts no unresolved exception. This makes negative tests fluent and keeps assertions on one chain. Note: exceptions that ARE resolved (e.g., mapped to a 400 by @ControllerAdvice) don't count as failures — you assert the resulting status/body normally. Only truly unresolved exceptions surface via hasFailed()/failure().
code
java · 12 lines// Unresolved exception: captured, assert fluently
assertThat(mvc.get().uri("/explode"))
.hasFailed()
.failure()
.isInstanceOf(IllegalStateException.class)
.hasMessageContaining("boom");
// Resolved by @ControllerAdvice -> it's a normal response, NOT a failure
assertThat(mvc.get().uri("/missing/1"))
.doesNotHaveFailed()
.hasStatus(HttpStatus.NOT_FOUND)
.bodyJson().extractingPath("$.error").isEqualTo("Not Found");go deeper
Know MockMvcTester lets you assert on exceptions with hasFailed() instead of the call throwing.
Distinguish resolved (normal response) vs unresolved (failure) exceptions.
Explain the classic rethrow difference and use failure() to assert type/message on one chain.
Guide teams on testing error contracts: assert resolved error responses normally, reserve failure assertions for intentionally unhandled paths.
## The classic MockMvc behavior With classic MockMvc, request processing runs through the `DispatcherServlet`. If a handler throws and a `HandlerExceptionResolver` (e.g., your `@ControllerAdvice` / `@ExceptionHandler`, or Boot's default error handling) **resolves** it into a response, you just get that response (say HTTP 400) and assert on it. But if **nothing resolves** the exception, MockMvc **rethrows** it out of `mockMvc.perform(...)`. That means the exception escapes the test statement, so to test it you must wrap with `assertThatThrownBy(() -> mockMvc.perform(...))` or a try/catch — you cannot chain response assertions because there is no clean response. ## The MockMvcTester behavior MockMvcTester **captures** an unresolved exception inside the `MvcTestResult` rather than throwing it out of the fluent call. This lets you keep a single AssertJ chain: - `.hasFailed()` — asserts the exchange failed with an **unresolved** exception. - `.doesNotHaveFailed()` — asserts it did not. - `.failure()` — returns an AssertJ `AbstractThrowableAssert`, so you can assert type/message: `.failure().isInstanceOf(IllegalStateException.class).hasMessageContaining("boom")`. Example: ```java assertThat(mvc.get().uri("/explode")) .hasFailed() .failure().isInstanceOf(IllegalStateException.class); ``` If you instead call `.exchange()` to get the `MvcTestResult`, note that reading certain properties on a failed result can rethrow — the assertions are the safe way to inspect failure. ## Key distinction: resolved vs unresolved - **Resolved** exception (mapped by an exception handler to a status/body): this is a normal response. `.hasFailed()` is **false**; assert the status/body (`.hasStatus(400).bodyJson()...`). - **Unresolved** exception (nothing handles it): captured; assert with `.hasFailed()`/`.failure()`. So whether you use failure assertions depends on whether your app has a handler. In a properly configured app, most exceptions are resolved to error responses, and you assert those. Failure assertions matter for controllers/paths you deliberately leave unhandled, or standalone tests without advice registered. ## Why this is nicer - **One fluent chain** for both success and failure cases — no mixing of assertThatThrownBy with response assertions. - **Readable intent**: `.hasFailed().failure().isInstanceOf(...)` reads clearly. - Avoids the classic surprise where `perform(...)` unexpectedly throws mid-test. ## Gotchas - Don't confuse a **4xx/5xx response** (resolved) with a **failure** (unresolved): a 500 produced by an error handler is not `hasFailed()`. - In standalone `of(...)` tests without your `@ControllerAdvice`, exceptions that would be resolved in production may surface as failures — register the advice to test the real behavior. - `.failure()` on a result that did **not** fail will itself fail the assertion (as expected).
- If a @ControllerAdvice maps the exception to a 400, should you use hasFailed()?No. A resolved exception produces a normal response; hasFailed() is false. Assert the status/body instead (.hasStatus(400).bodyJson()...). hasFailed() is only for exceptions nothing resolves.
- How would you test the same unresolved-exception case with classic MockMvc?Because perform(...) rethrows, you'd wrap it: assertThatThrownBy(() -> mockMvc.perform(get("/explode"))).hasCauseInstanceOf(IllegalStateException.class) or a try/catch — you can't chain response assertions.
saying these in an interview costs you the question
- Treating a resolved 4xx/5xx response as hasFailed()
- Thinking assertThat(...) rethrows like classic perform(...)
- Using failure() when an exception handler produced a normal response