async
spring
MockMvc
flaky tests
spring controller

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.

java
1import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
2import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
3import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
4import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
5import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
6
7MvcResult result = mockMvc.perform(get("/api/async-value"))
8        .andExpect(request().asyncStarted())
9        .andReturn();
10
11mockMvc.perform(asyncDispatch(result))
12        .andExpect(status().isOk())
13        .andExpect(content().string("done"));

This pattern removes race conditions caused by premature assertions.

Example Async Controller

A small endpoint using CompletableFuture:

java
1@RestController
2@RequestMapping("/api")
3class AsyncController {
4
5    @GetMapping("/async-value")
6    public CompletableFuture<String> value() {
7        return CompletableFuture.supplyAsync(() -> "done");
8    }
9}

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.

java
1@TestConfiguration
2class AsyncTestConfig {
3    @Bean
4    public Executor taskExecutor() {
5        ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
6        ex.setCorePoolSize(1);
7        ex.setMaxPoolSize(1);
8        ex.setQueueCapacity(10);
9        ex.setThreadNamePrefix("test-async-");
10        ex.initialize();
11        return ex;
12    }
13}

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.

java
1@TestPropertySource(properties = {
2    "spring.mvc.async.request-timeout=5000"
3})
4class AsyncControllerTest {
5}

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.

java
mockMvc.perform(asyncDispatch(result))
        .andExpect(status().is5xxServerError());

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 perform call without asyncDispatch.
  • 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 MockMvc flow.
  • Always assert asyncStarted, then finalize with asyncDispatch.
  • 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.

Course illustration
Course illustration

All Rights Reserved.