When do you use andReturn/MvcResult, how do you test async controller methods, and what are MockMvc's fundamental limitations?
answer
- andReturn() -> MvcResult -> getResponse().getContentAsString()
- async: asyncStarted() then asyncDispatch(mvcResult)
- forgetting asyncDispatch = empty body
- no socket / no real container / no true streaming
- real HTTP -> WebTestClient / TestRestTemplate @ RANDOM_PORT
basics
~20 sandReturn() gives you the MvcResult for custom assertions on the raw response. For async endpoints (returning DeferredResult/Callable/CompletableFuture), first andExpect(request().asyncStarted()), then re-dispatch with asyncDispatch(mvcResult). MockMvc's key limit: it runs the DispatcherServlet in-memory, not a real HTTP server.
solid answer
~40 sandReturn() ends the fluent chain and returns the MvcResult, exposing the raw MockHttpServletResponse, the resolved handler, any thrown exception, and the model — use it for assertions matchers can't express, or to capture output (e.g. parse getResponse().getContentAsString() with your ObjectMapper). Async controllers (returning Callable, DeferredResult, CompletableFuture, or WebAsyncTask) return before the result is ready, so a single perform only starts async processing; you assert request().asyncStarted(), capture the MvcResult via andReturn(), then re-dispatch: mockMvc.perform(asyncDispatch(mvcResult)).andExpect(status().isOk()). Fundamental limits: MockMvc drives DispatcherServlet in-process with mock request/response, so there's no real socket, no genuine HTTP/servlet-container behavior, no true streaming or connection semantics, and container-level concerns (multipart size limits, real filters ordering, Tomcat quirks) aren't exercised. For end-to-end HTTP use WebTestClient or TestRestTemplate against a RANDOM_PORT server. Note MockMvc also has a WebTestClient adapter (bindToController/bindToApplicationContext).
code
java · 15 lines@Test
void asyncEndpointCompletes() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/reports/slow"))
.andExpect(request().asyncStarted())
.andReturn();
mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("DONE"));
}
// controller:
// @GetMapping("/reports/slow")
// CompletableFuture<Report> slow() { return service.buildAsync(); }
// static import: MockMvcRequestBuilders.asyncDispatch,
// MockMvcResultMatchers.requestgo deeper
Knows andReturn() yields something to read the body from.
Uses MvcResult to parse JSON and knows async needs special handling.
Correctly applies asyncStarted + asyncDispatch and knows the no-real-HTTP boundary.
Designs the test-pyramid split between MockMvc slices and real-port end-to-end tests, weighing fidelity vs speed.
**`andReturn()` and `MvcResult`** `ResultActions.andReturn()` terminates the matcher chain and returns the **`MvcResult`** — the full record of what happened during dispatch. From it you can access: - `getResponse()` → the `MockHttpServletResponse` (status, headers, `getContentAsString()`, cookies). - `getRequest()` → the `MockHttpServletRequest`. - `getHandler()` → the resolved handler method. - `getResolvedException()` → any exception handled by the framework. - `getModelAndView()` → model/view for view-based responses. - `getAsyncResult()` → the produced async value. Use `andReturn()` when built-in `ResultMatcher`s can't express the assertion — e.g., deserialize the JSON body into a DTO and assert on typed fields, feed the response into another test step, or make dynamic assertions. Pattern: ```java MvcResult result = mockMvc.perform(get("/users/42")) .andExpect(status().isOk()) .andReturn(); String body = result.getResponse().getContentAsString(); User user = objectMapper.readValue(body, User.class); assertThat(user.name()).isEqualTo("Ada"); ``` **Testing async controllers** Spring MVC supports async return types: `Callable<T>`, `DeferredResult<T>`, `CompletableFuture<T>`/`ListenableFuture`, `WebAsyncTask<T>`, and streaming types (`ResponseBodyEmitter`, `SseEmitter`, `StreamingResponseBody`). For these, the controller method returns *before* the value is produced, and Spring's async request lifecycle kicks in. A single `perform(...)` only **starts** async processing — the response body isn't ready yet. The correct two-phase pattern: ```java MvcResult mvcResult = mockMvc.perform(get("/slow")) .andExpect(request().asyncStarted()) // async began .andReturn(); // (optionally) request().asyncResult(...) to assert the produced value mockMvc.perform(asyncDispatch(mvcResult)) // re-dispatch .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("Ada")); ``` - `request().asyncStarted()` (a `RequestResultMatchers`) asserts async processing began. - `request().asyncResult(expected)` can assert the value the async type resolved to (it waits for completion). - `MockMvcRequestBuilders.asyncDispatch(mvcResult)` builds a request that re-enters the dispatcher to render the now-available result — this is what actually produces the final response you assert on. Skipping `asyncDispatch` is the classic mistake: the first `perform` shows an empty body / 200 with no content because the async result hasn't been rendered. **Fundamental limitations of MockMvc** MockMvc is a *server-side, in-process* test of the Spring MVC layer. What it is **not**: - **No real HTTP / no socket:** it invokes `DispatcherServlet.service(...)` with `MockHttpServletRequest`/`MockHttpServletResponse`. Nothing crosses the network. Real HTTP parsing, connection handling, chunked transfer, keep-alive, HTTP/2 — none are exercised. - **No real servlet container:** Tomcat/Jetty specific behavior, container-enforced multipart size limits (`spring.servlet.multipart.max-file-size`), error-page mapping, and real filter-chain wiring aren't fully represented (filters run only if registered/auto-configured). - **Streaming is limited:** SSE/`StreamingResponseBody` don't stream over a wire; you inspect buffered output, not incremental delivery/backpressure. - **Not client-side and not reactive:** it doesn't test WebClient calls or WebFlux endpoints (those use `WebTestClient`). **When to reach past MockMvc:** for genuine end-to-end HTTP confidence, use `@SpringBootTest(webEnvironment = RANDOM_PORT)` with **`TestRestTemplate`** or **`WebTestClient`** (bound to a real port) — these open real connections. Notably, `WebTestClient` can also *bind to MockMvc* (`MockMvcWebTestClient.bindTo...`), giving the fluent WebTestClient API over the in-process server, but that still doesn't add real-socket fidelity. **Architectural framing (principal-level):** treat MockMvc as the fast, high-volume tier for controller contract/behavior (status codes, validation, serialization, security rules), and reserve a thin layer of real-port tests for the handful of concerns only a real container reveals. Over-investing in full `@SpringBootTest` HTTP tests for everything is slow; relying solely on `standaloneSetup` risks config drift. The right mix is MockMvc slices (`@WebMvcTest`) plus a few true end-to-end checks.
- Why does a single perform() on a CompletableFuture-returning endpoint give an empty response?The controller returns before the future completes, so perform only starts async processing. You must capture the MvcResult and re-dispatch via asyncDispatch(mvcResult) to render the now-available result and assert on the real body.
- When would you choose WebTestClient/TestRestTemplate over MockMvc?When you need real HTTP over a socket — @SpringBootTest(webEnvironment=RANDOM_PORT). This exercises actual container behavior, connection handling, and streaming that MockMvc's in-process DispatcherServlet dispatch cannot reproduce.
- What does MvcResult give you that ResultMatchers don't?Direct access to the raw MockHttpServletResponse, the resolved handler, any resolved exception, the model, and the async result — enabling custom/typed assertions, response parsing, and chaining that the fluent matchers can't express.
saying these in an interview costs you the question
- Asserting on the async body from the first perform without asyncDispatch
- Claiming MockMvc opens a real port / exercises the servlet container
- Thinking MockMvc can test WebFlux/reactive endpoints (that's WebTestClient)
- Believing MockMvc enforces container multipart size limits