Testing Spring Controllers in Spring Boot (Old vs Latest) + WebFlux + CI/CD Guidance
This document explains the different ways to test Spring controllers in Spring Boot, how testing approaches have evolved with newer versions, how to handle Spring WebFlux reactive controllers, and which tests are safe to run in CI/CD pipelines without slowing or breaking them.
1. Evolution of Testing Approaches
Older Spring Boot Versions (2.x and before WebFlux)
- Focused primarily on Spring MVC controllers.
- Common test methods:
- Unit Tests: JUnit + Mockito, testing controllers directly without Spring context.
- MockMvc: For testing request mappings, validation, and serialization.
- @WebMvcTest: Slice tests for the web layer with controller + Spring MVC infra.
- @SpringBootTest: Full context tests including controllers, services, repositories.
- TestRestTemplate: End-to-end HTTP-level tests.
Newer Spring Boot Versions (3.x + Spring 6)
- Added Spring WebFlux (reactive programming) alongside MVC.
- Testing tools expanded:
- WebTestClient: For testing WebFlux controllers and also usable for MVC.
- StepVerifier: For verifying
Mono/Fluxsequences in detail.
- Emphasis on slice tests to reduce startup cost.
- Testcontainers commonly used for integration tests in CI pipelines.
Recent Updates
- Core testing philosophy hasn't changed, but:
- Stronger module boundaries in Spring Boot 3.x.
- Better support for reactive testing utilities.
- Wider use of contract/API tests (Spring Cloud Contract, OpenAPI validation).
2. Ways to Test Spring Controllers
Approach Target Scope Tools/Annotations Pros Cons
Unit Tests Controller methods JUnit, Mockito Fastest, no Doesn't test HTTP in isolation Spring overhead mapping/serialization
Slice Tests (Web Controller + @WebMvcTest + Tests routing, Services must be mocked
Layer) Spring MVC/WebFlux MockMvc, or validation,
infra @WebFluxTest + serialization
WebTestClient
Integration Full Spring @SpringBootTest + Ensures wiring Slower, more
Tests context MockMvc or correctness dependencies
(controller, WebTestClient
services, repos)
End-to-End Real HTTP calls TestRestTemplate, Maximum realism Slowest, may need
Tests against running WebTestClient external infra
app
Contract/API Validate Spring Cloud Contract, Ensures API Doesn't test internal Tests request/response REST Docs, OpenAPI compatibility logic contracts
Behavioral/BDD Full user journeys Cucumber, Karate, Captures real Heavy, harder to Tests RestAssured scenarios maintain
Reactive Validate reactive StepVerifier Accurate async Extra complexity,
Sequence Tests flows behavior scheduler mgmt
validation
3. MVC vs WebFlux Differences
Concern MVC Controllers WebFlux Controllers
Test client MockMvc WebTestClient
Return type ResponseEntity<T> or Mono<T> / Flux<T>
POJO
Execution Blocking Non-blocking
Verification Assertions on return Use StepVerifier,
values expectNext, expectComplete
Time control Rarely needed May use VirtualTimeScheduler
for delays and async ops
4. CI/CD Guidance
Recommended for CI/CD Pipelines
- Unit tests: very fast feedback.
- Slice tests (
@WebMvcTest,@WebFluxTest): lightweight and stable. - Integration tests with Testcontainers: simulate DB, Kafka, etc. reliably.
- Contract/API tests: ensure backward compatibility.
Caution for CI/CD
- Heavy end-to-end tests may slow pipelines.
- External dependencies (real DBs, APIs) increase flakiness.
- Full BDD/acceptance suites are better in nightly builds or staging.
Rule of thumb: keep CI/CD tests under a few minutes; delegate exhaustive tests to separate pipelines.
5. Sample Code Skeletons
Unit Test (no Spring context)
class MyControllerUnitTest {
private final MyService service = mock(MyService.class);
private final MyController controller = new MyController(service);
@Test
void testFoo() {
when(service.getValue()).thenReturn("ok");
assertEquals("ok", controller.foo());
}
}
Slice Test (WebFlux)
@WebFluxTest(MyController.class)
class MyControllerSliceTest {
@Autowired WebTestClient webTestClient;
@MockBean MyService service;
@Test
void testFooEndpoint() {
when(service.getValue()).thenReturn("ok");
webTestClient.get().uri("/foo")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("ok");
}
}
Integration Test (Spring Boot + Reactive)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class MyControllerIntegrationTest {
@Autowired WebTestClient webTestClient;
@Test
void testFooIntegration() {
webTestClient.get().uri("/foo")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.result").isEqualTo("ok");
}
}
Reactive Sequence Validation
Flux<Integer> numbers = Flux.range(1, 3).map(i -> i * 2);
StepVerifier.create(numbers)
.expectNext(2, 4, 6)
.expectComplete()
.verify();
✅ Summary
- You can test controllers via unit, slice, integration, end-to-end, contract, BDD, and reactive tests.\
- Across Spring Boot versions, the main difference is WebFlux
support with
WebTestClientandStepVerifier.\ - CI/CD pipelines should prioritize unit, slice, and lightweight integration tests, with heavier E2E/BDD deferred to staging/nightly builds.