Writing integration tests for HTTP endpoints has always involved a bit of ceremony. You set up the context, choose between MockMvc, WebTestClient, or TestRestTemplate, write the request, and wrestle with assertions. Spring Boot 4.x introduces RestTestClient to cut that friction significantly.
Table of contents
Open Table of contents
The problem with older versions (Spring Boot 3.x and earlier)
Testing HTTP endpoints in Spring Boot 3.x worked, but every option came with its own trade-offs.
MockMvc was powerful but verbose
MockMvc tied tests to the DispatcherServlet internals. Even a simple GET request check required several chained perform() calls and result matchers that were hard to read at a glance.
mockMvc.perform(get("/orders/42")
.header("Authorization", "Bearer test-token"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(42))
.andExpect(jsonPath("$.status").value("PLACED"));
Fine for one test, but across dozens of tests this syntax grew noisy.
WebTestClient pulled in reactive dependencies
WebTestClient gave a fluent API and was easier to read, but it was designed around the reactive stack. Using it in a non-reactive application added spring-webflux as a test dependency, which felt wrong.
TestRestTemplate gave weak assertions
TestRestTemplate was the simplest option but offered no fluent assertion API. You had to extract the response, cast it manually, and write assertion logic yourself.
ResponseEntity<OrderResponse> response =
testRestTemplate.getForEntity("/orders/42", OrderResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody().getId()).isEqualTo(42);
No built-in chaining, no header assertions, no body-level matchers.
Important: Using different test clients across your test suite creates inconsistency. When someone joins the team, they have to learn multiple patterns just to write new tests.
What Spring Boot 4.x changes
Spring Boot 4.x introduces RestTestClient — a test-focused wrapper built on top of RestClient. It brings the fluent, readable style of WebTestClient to blocking HTTP tests, without pulling in reactive dependencies.
Add the auto-configuration annotation
Use @AutoConfigureRestTestClient alongside @SpringBootTest to get a configured RestTestClient injected into your test.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class OrderControllerTest {
@Autowired
RestTestClient restTestClient;
}
Write readable request-response chains
The fluent API keeps the request and assertion on a single logical flow.
@Test
void shouldReturnOrder() {
restTestClient.get()
.uri("/orders/42")
.header("Authorization", "Bearer test-token")
.exchange()
.expectStatus().isOk()
.expectBody(OrderResponse.class)
.value(order -> {
assertThat(order.id()).isEqualTo(42);
assertThat(order.status()).isEqualTo("PLACED");
});
}
Test error responses just as easily
The same chain works for 4xx and 5xx cases without switching test client or approach.
@Test
void shouldReturn404WhenOrderNotFound() {
restTestClient.get()
.uri("/orders/999")
.exchange()
.expectStatus().isNotFound()
.expectBody()
.jsonPath("$.message").isEqualTo("Order not found");
}
Tip: Use
RANDOM_PORTwithRestTestClientso tests run against a real server. This catches issues like missing security filters and interceptors that in-memory testing can miss.
Old way vs new way
| Area | Older versions | Spring Boot 4.x |
|---|---|---|
| Fluent API for blocking tests | Not available | RestTestClient |
| Reactive dependency for fluent tests | Required (spring-webflux) | Not needed |
| Assertion style | Result matchers or manual casting | Chained expectBody() |
| Consistency across tests | Multiple client options | One standard client |
Practical tips for rollout
- Start by rewriting the highest-value integration tests first — the ones that catch real regressions.
- Keep
MockMvctests for controller unit tests where you want to test binding and validation only. - Use
RANDOM_PORTfor tests that must go through filters, interceptors, and security. - Name test methods to describe what the response should be, not what the request is.
Caution: Do not replace every
MockMvcunit test withRestTestClientintegration tests. Integration tests are slower and should focus on end-to-end behavior. Unit tests should stay fast and targeted.
Migration checklist from older projects
- Identify integration tests that use
TestRestTemplatewith manual assertions. - Add
@AutoConfigureRestTestClientto those test classes. - Inject
RestTestClientand replacegetForEntity()calls with the fluent chain. - Move header setup into a shared
beforeEachmethod if multiple tests share the same headers. - Replace
jsonPath()matchers with typedexpectBody(SomeClass.class)where possible. - Run the test suite and confirm the same scenarios pass.
Note: A better test client makes writing tests easier, but test coverage still comes from thinking carefully about which cases matter — the client is just the tool.