Explain MockMvcTester's lazy exchange model and when you'd adopt it versus classic MockMvc or WebTestClient.
answer
- Builder is AssertProvider -> runs on assertThat(...)
- No assert = request never executes
- exchange() = run once, assert many
- Same mock servlet engine as MockMvc (style, not transport)
- WebTestClient for real HTTP / WebFlux
basics
~20 sThe request builder is an AssertJ AssertProvider, so the request only runs when you call assertThat(...) or .exchange(). Adopt MockMvcTester for readable AssertJ servlet-slice tests; keep classic MockMvc where it's already entrenched; use WebTestClient for real-HTTP/WebFlux or full-stack tests.
solid answer
~50 sMockMvcTester's method builders (mvc.get().uri(...)) implement AssertJ's AssertProvider<MvcTestResultAssert>, so the request isn't performed when you build it — it executes lazily the moment you pass it to assertThat(...), which returns the assert. You can also force execution with .exchange() to hold an MvcTestResult and make multiple assertions on it, and reuse a builder across assertions. Adoption guidance: MockMvcTester is the modern default for servlet-stack (Spring MVC) controller/slice tests when you want AssertJ readability, shared JSON assertions with @JsonTest, and clean exception capture; it's the same mock engine, so it's a drop-in style upgrade from perform(...). Keep classic MockMvc where a large suite already uses it — mixing is fine. Choose WebTestClient (or a real client against a RANDOM_PORT server) when you need actual HTTP semantics, WebFlux, or full-stack/end-to-end coverage — MockMvcTester never opens a socket. It also handles async results, waiting for completion automatically.
code
java · 10 lines// Lazy: this performs the request when assertThat runs
assertThat(mvc.get().uri("/x")).hasStatusOk();
// Explicit exchange: run once, assert multiple times
MvcTestResult result = mvc.get().uri("/users/1").exchange();
assertThat(result).hasStatusOk();
assertThat(result).bodyJson().extractingPath("$.id").isEqualTo(1);
// Pitfall: builder built but never asserted -> request NEVER runs
var ignored = mvc.get().uri("/never-called"); // does nothing on its owngo deeper
Know the request runs when you assert, and MockMvcTester doesn't hit a real server.
Understand exchange() vs assertThat and that it's the same engine as MockMvc.
Explain the AssertProvider lazy model, async handling, and pick between MockMvcTester and WebTestClient.
Set org-wide testing conventions: MockMvcTester as MVC-slice default, WebTestClient for reactive/full-stack, and a pragmatic migration policy from classic MockMvc.
## Lazy exchange model When you write `mvc.get().uri("/x")`, you get a request builder that **also implements AssertJ's `AssertProvider<MvcTestResultAssert>`**. AssertJ's `assertThat(AssertProvider)` overload calls back into the provider to produce the actual assert — and it is at that moment MockMvcTester **performs** the request against the mock servlet stack and returns an `MvcTestResultAssert`. Consequences: - **Nothing happens until you assert.** If you build a request and never pass it to `assertThat(...)` (or call `.exchange()`), the request never runs — a subtle way to write a test that silently does nothing. - **Explicit exchange:** `MvcTestResult result = mvc.get().uri("/x").exchange();` performs the request once and lets you make **multiple** independent assertions on the same result: `assertThat(result).hasStatusOk(); assertThat(result).bodyJson()...;`. - **Async:** for `DeferredResult`/`Callable`/`CompletableFuture`/streaming handlers, MockMvcTester waits for async completion before producing the result, so you don't manually do `asyncDispatch` as often as with classic MockMvc. ## Same engine, different surface Crucially, MockMvcTester is **not a new transport** — it wraps the exact same `MockMvc`/`DispatcherServlet` machinery. So semantics (filters, converters, advice, security) are identical to classic MockMvc; only the assertion ergonomics change. That makes it a **style** decision, not an architectural one, and mixing MockMvcTester and classic MockMvc in the same codebase is fine. ## Adoption decision matrix | Goal | Recommendation | |---|---| | New Spring MVC controller/slice test, want AssertJ | **MockMvcTester** | | Consistent JSON asserts with `@JsonTest` | MockMvcTester (`bodyJson()` = same `JsonContentAssert`) | | Assert on unresolved exceptions cleanly | MockMvcTester (`hasFailed()/failure()`) | | Huge existing `perform(...)` suite | Keep classic MockMvc; migrate opportunistically | | WebFlux / reactive endpoints | **WebTestClient** | | Real HTTP semantics, full stack, e2e | WebTestClient / RestClient against `@SpringBootTest(webEnvironment=RANDOM_PORT)` | ## Why MockMvcTester over classic for new code - **Readability & discoverability:** AssertJ chains + IDE completion. - **Unified assertions:** same JSON assert as serializer tests. - **Better negative tests:** exception capture instead of rethrow. - **Lower ceremony:** Boot 3.4 injects it; no `perform().andExpect()` boilerplate. ## Why sometimes not - **No new capability:** it can't test anything the servlet mock can't (no real network, HTTP/2, TLS, connection pools). - **Migration cost:** rewriting a stable, large suite for style alone is low ROI. - **Team familiarity:** if the team knows Hamcrest matchers well, the payoff is smaller. ## Gotchas - **Forgotten assertion = no execution** because of lazy exchange; ensure every request is asserted or `.exchange()`d. - Reusing a builder across many `assertThat(...)` calls performs the request **each time**; use `.exchange()` once if you want a single execution with multiple assertions. - Still mock-stack: don't reach for MockMvcTester to validate real HTTP/proxy/filter-chain-at-network behavior.
- You build a request but the endpoint is never hit and the test still passes. Why?Lazy exchange: the builder only performs the request when passed to assertThat(...) or when .exchange() is called. If you never assert, nothing executes, so no failure — a silent no-op test.
- When would MockMvcTester be the wrong tool?When you need real HTTP semantics or reactive endpoints: it never opens a socket and targets the servlet stack. Use WebTestClient (WebFlux/real-HTTP) or a client against a RANDOM_PORT @SpringBootTest for full-stack/e2e coverage.
- How do you run the request once but assert on it several times?Call .exchange() to get an MvcTestResult, then pass that result to multiple assertThat(...) calls; each assertion reuses the single performed result instead of re-executing.
saying these in an interview costs you the question
- Believing the request runs when the builder is constructed
- Reusing a builder across asserts assuming one execution
- Thinking MockMvcTester can test real HTTP/WebFlux behavior