Flaky tests of async spring controller with MockMvc
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
Async Spring MVC controller tests can look correct but still fail intermittently when the test does not complete the async lifecycle. MockMvc requires a two-step test flow for async endpoints, and skipping the second dispatch is a common source of flakiness. Stable tests also depend on deterministic executors and predictable timeouts.
Why Async Tests Become Flaky
A controller method returning Callable, DeferredResult, or CompletableFuture starts async processing. The first perform call only verifies request acceptance, not final response body.
If assertions run too early, tests pass or fail depending on thread timing.
Correct MockMvc Pattern for Async Endpoints
Use async start checks, then call asyncDispatch with the same MvcResult.
This pattern removes race conditions caused by premature assertions.
Example Async Controller
A small endpoint using CompletableFuture:
If your app uses custom executors, mirror that behavior in test configuration to avoid timing drift.
Use Deterministic Executors in Tests
Unbounded thread pools and shared executors can make tests non-deterministic. For test slices, use a small controlled executor.
A single worker can dramatically reduce ordering surprises.
Timeouts and Async Request Settings
Default async timeout values may be too low for CI hosts under load. Set explicit test-friendly limits.
Prefer bounded but realistic values instead of indefinite waits.
Verify Error Paths Too
Flakiness sometimes hides in exceptional async flows. Add tests for failure completion.
This ensures global exception handlers and async wrappers behave consistently.
Keep Test Scope Focused
If your controller depends on remote services, use mocks for service layers in @WebMvcTest. Full integration tests are still valuable, but unit style controller tests should not depend on network timing.
Separating concerns makes failures easier to diagnose and keeps CI cycle times stable.
CI Stability Techniques
If flakes still occur, isolate async controller tests into a separate suite and run them with controlled parallelism. Capturing thread dumps and request logs on failure can reveal hidden contention or accidental blocking calls. Stable async testing is less about one assertion and more about a predictable execution environment in both local and CI pipelines.
A helpful pattern is adding one targeted integration test that exercises the same endpoint through the full HTTP stack. This does not replace focused MockMvc tests, but it confirms container, serialization, and async wiring all behave together under realistic configuration. That extra signal can identify whether flakes are test harness issues or real runtime issues.
Record async timeout values as explicit team defaults.
Common Pitfalls
- Asserting response content after the first
performcall withoutasyncDispatch. - Using shared thread pools in tests, causing cross-test interference.
- Depending on machine speed and leaving async timeout defaults unreviewed.
- Testing controller async behavior while also hitting real external dependencies.
- Ignoring async exception paths and only testing success response timing.
Summary
- Async Spring controller tests require a two-step
MockMvcflow. - Always assert
asyncStarted, then finalize withasyncDispatch. - Use deterministic executors and explicit timeout settings in test configuration.
- Mock external dependencies to remove non-deterministic latency.
- Cover both success and failure async paths for stable CI behavior.

