A controller returning Callable or another async value starts servlet async processing; the first MockMvc result is not the completed HTTP response.
Spring MockMvc async dispatch: assert the final response after resumed handling
Two phases of one request
A receipt export endpoint returns a Callable that produces a response later. MockMvc first sees the request enter async mode. The application then completes the computation and the servlet request is dispatched again so MVC can render or resolve an exception. A test that only asserts asyncStarted misses the final status, headers and body. Use asyncDispatch on the captured MvcResult and make the second assertion the HTTP contract. MockMvc still uses mock servlet objects, so this is not a real socket test.
Test failure as well as success
If export generation throws after the initial return, the async dispatch is where an exception resolver can turn it into an error response. Assert the error shape there. Use an executor appropriate to the test: a direct executor can make a unit test deterministic but will not demonstrate production concurrency or rejection. Executor admission and background failure handling need their own tests.
Keep waiting bounded
Set a test wait budget that covers the controlled calculation, not an open-ended sleep. A test should assert that async processing started before dispatch; otherwise a controller change to synchronous behavior may silently alter the boundary. For timeout behavior, test the configured MVC timeout and resulting response separately from the work cancellation policy. An async Servlet response can time out while underlying work continues unless that work is cancelled cooperatively.
Implementation contract
MvcResult pending = mvc.perform(get("/api/receipts/r-47/export"))
.andExpect(request().asyncStarted())
.andReturn();
mvc.perform(asyncDispatch(pending))
.andExpect(status().isOk())
.andExpect(header().string("Content-Type", "application/pdf"));Cost and verification
The test has two dispatch phases and one bounded wait. It does not measure thread-pool throughput or socket latency; those require executor and live-server checks.
Common Mistakes
- Do not assert only asyncStarted and call the response verified.
- Do not use Thread.sleep as the synchronization mechanism.
- Do not assume a response timeout automatically stops the background calculation.
Read next
Spring MockMvc tests: HTTP behavior without claiming a real network test, Spring @Async futures: make worker failure observable to the caller, Spring task executors: reject work when every slot is occupied, Test Spring RestClient on a socket without claiming a remote service, Spring test failures: prove the layer that can reject the request.
