Skip to content
JSBlogs
Go back

RestTestClient in Spring Boot 4.x for cleaner HTTP tests

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_PORT with RestTestClient so 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

AreaOlder versionsSpring Boot 4.x
Fluent API for blocking testsNot availableRestTestClient
Reactive dependency for fluent testsRequired (spring-webflux)Not needed
Assertion styleResult matchers or manual castingChained expectBody()
Consistency across testsMultiple client optionsOne standard client

Practical tips for rollout

  1. Start by rewriting the highest-value integration tests first — the ones that catch real regressions.
  2. Keep MockMvc tests for controller unit tests where you want to test binding and validation only.
  3. Use RANDOM_PORT for tests that must go through filters, interceptors, and security.
  4. Name test methods to describe what the response should be, not what the request is.

Caution: Do not replace every MockMvc unit test with RestTestClient integration 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

  1. Identify integration tests that use TestRestTemplate with manual assertions.
  2. Add @AutoConfigureRestTestClient to those test classes.
  3. Inject RestTestClient and replace getForEntity() calls with the fluent chain.
  4. Move header setup into a shared beforeEach method if multiple tests share the same headers.
  5. Replace jsonPath() matchers with typed expectBody(SomeClass.class) where possible.
  6. 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.

References


Share this post on:

Previous Post
JmsClient in Spring Boot 4.x for cleaner messaging code
Next Post
OpenTelemetry starter in Spring Boot 4.x for easier observability