Enterprise Java

Getting Started with Spring RestTestClient

Testing REST APIs in Spring has traditionally used several tools, such as MockMvc, WebTestClient, and TestRestTemplate, each offering its own API and use cases. With Spring Framework 7 and Spring Boot 4, a new unified testing client, RestTestClient, was introduced. It brings together the strengths of these tools into a consistent, fluent API suitable for unit, integration, and end-to-end HTTP tests. This article provides a guide to using Spring RestTestClient and examples for testing REST APIs.

1. What Is RestTestClient?

RestTestClient is a testing-oriented HTTP client built on top of Spring’s RestClient. At its core, it performs HTTP requests and then provides a fluent API to assert on the response. In short, RestTestClient enables us to perform HTTP requests against a mock server, assert response status, headers, and bodies, use the same API across various testing scenarios, and test controllers, full Spring contexts, as well as functional endpoints.

2. Setting Up RestTestClient

To use RestTestClient, include these in your pom.xml (Maven):

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-webmvc-test</artifactId>
			<scope>test</scope>
		</dependency>
Note
Before testing, make sure your project uses Spring Framework 7 or later and Spring Boot 4, as RestTestClient is a newer addition.

3. How RestTestClient is Built

We start by binding the client to some server context:

RestTestClient client = RestTestClient.bindToController(new MyController()).build();

This “bindTo*” pattern determines how RestTestClient executes requests:

  • Controller only (fastest, no Spring context)
  • MockMvc (MVC layer with Spring validation/security)
  • ApplicationContext (full Spring context but no running HTTP server)
  • RouterFunction (functional endpoints)
  • Live server (end-to-end HTTP tests)

4. Testing Strategies

4.1 Unit Test: bindToController

Unit testing allows us to test controller logic in isolation without starting the full Spring context or server.

Controller Example

@RestController
@RequestMapping("/books")
public class BookController {

    @GetMapping("/{id}")
    public ResponseEntity<Book> getBook(@PathVariable Long id) {
        if (id == 1L) {
            return ResponseEntity.ok(new Book(id, "Spring Guide", "Thomas P"));
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}

public record Book(Long id, String title, String author) { }

Test Class

public class BookControllerTest {

    RestTestClient client;

    @BeforeEach
    void setUp() {
        client = RestTestClient.bindToController(new BookController()).build();
    }

    @Test
    void testGetBookFound() {
        client.get().uri("/books/1")
                .exchange()
                .expectStatus().isOk()
                .expectBody(Book.class)
                .value(book -> assertEquals("Spring Guide", book.title()));
    }

    @Test
    void testGetBookNotFound() {
        client.get().uri("/books/99")
                .exchange()
                .expectStatus().isNotFound();
    }
}

The first test validates that a valid book ID returns a 200 OK with the expected book. The second test checks that an invalid ID returns a 404 Not Found.

4.2 MVC Test: bindToMockMvc

When we need to test Spring MVC features like validation, filters, or security, we bind to a MockMvc instance.

@WebMvcTest(BookController.class)
class BookControllerMvcTest {

    @Autowired MockMvc mockMvc;

    @Test
    void testBookNotFound() {
        RestTestClient client = RestTestClient.bindTo(mockMvc).build();

        client.get().uri("/books/99")
              .exchange()
              .expectStatus().isNotFound();
    }
}

By binding to MockMvc, the test simulates the full MVC stack without running a real HTTP server. This allows testing validation, error handling, and annotations in the controller while keeping execution fast. The example checks that requesting a non-existent book returns a 404 Not Found.

4.3 ApplicationContext Integration Test

Integration tests verify behaviour across services, repositories, and controllers using a full Spring context but no actual HTTP server.

@SpringBootTest
class BookControllerIntegrationTest {

    @Autowired
    WebApplicationContext context;
    RestTestClient client;

    @BeforeEach
    void setUp() {
        client = RestTestClient.bindToApplicationContext(context).build();
    }

    @Test
    void testGetBookIntegration() {
        client.get().uri("/books/1")
                .exchange()
                .expectStatus().isOk()
                .expectBody(Book.class)
                .value(book -> assertEquals("Thomas P", book.author()));
    }
}

Binding to the full application context allows the test to exercise controllers, services, and repositories together. This approach is ideal for verifying end-to-end application behaviour without starting an actual HTTP server.

4.4 End-to-End Test: bindToServer

End-to-end tests involve running a server and testing the API as clients would consume it over HTTP.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class BookControllerE2ETest {

    @LocalServerPort
    int port;

    @Test
    void testBookEndpoint() {
        RestTestClient client = RestTestClient.bindToServer()
                .baseUrl("http://localhost:" + port)
                .build();

        client.get().uri("/books/1")
                .exchange()
                .expectStatus().isOk()
                .expectBody(Book.class)
                .value(book -> assertEquals(1L, book.id()));
    }
}

This test connects to a live server using the real HTTP protocol, validating full request-response cycles. It is useful for testing CORS, compression, or other network-level behaviour that cannot be verified in controller-only or mock-based tests.

4.5 Functional Endpoint Test

For Spring WebFlux or functional routing, we can bind RestTestClient to a router function.

class BookFunctionalEndpointTest {

    @Test
    void shouldReturnGreetingFromFunctionalEndpoint() {

        RouterFunction routes
                = RouterFunctions.route(
                        RequestPredicates.GET("/books/hello"),
                        request -> ok().body("Hello Books")
                );

        RestTestClient client
                = RestTestClient.bindToRouterFunction(routes).build();

        client.get().uri("/books/hello")
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class)
                .value(response -> assertEquals("Hello Books", response));
    }
}

This approach tests functional endpoints without requiring controllers or a full application context. It is especially useful in reactive applications, enabling lightweight and fast validation of routing logic.

Testing Multiple Controllers

In some scenarios, we may want to test how multiple controllers behave together without loading the full Spring application context. RestTestClient allows us to bind more than one controller instance at the same time, making it easy to validate interactions and routing across related endpoints while keeping tests fast and lightweight.

@RestController
@RequestMapping("/authors")
class AuthorController {

    @GetMapping("/{name}")
    public ResponseEntity<String> getAuthor(@PathVariable String name) {
        return ResponseEntity.ok(name);
    }
}

Test Example Using Multiple Controllers

class MultipleControllersTest {

    RestTestClient client;

    @BeforeEach
    void setUp() {
        client = RestTestClient.bindToController(new BookController(), new AuthorController()).build();
    }

    @Test
    void shouldReturnBookFromBookController() {
        client.get().uri("/books/1")
                .exchange()
                .expectStatus().isOk()
                .expectBody(Book.class)
                .value(book -> assertEquals("Spring Guide", book.title()));
    }

    @Test
    void shouldReturnAuthorFromAuthorController() {
        client.get().uri("/authors/Thomas")
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class)
                .value(author -> assertEquals("Thomas", author));
    }
}

In this example, two separate controllers, BookController and AuthorController, are bound to a single RestTestClient instance. This allows the test to exercise endpoints from both controllers without starting a full Spring context or HTTP server. Each test method verifies that requests are routed to the correct controller and that the expected responses are returned. This approach is useful when validating related controllers together while still benefiting from fast execution times.

5. Conclusion

In this guide, we explored Spring RestTestClient as a modern and unified approach to testing REST APIs across different layers of an application. We covered how to use it for controller unit tests, MVC-based tests, full application context integration, functional endpoints, end-to-end HTTP testing, and scenarios involving multiple controllers. By leveraging its fluent API and flexible binding options, RestTestClient enables us to write clear, consistent, and maintainable tests while choosing the right level of isolation or integration for each testing scenario.

6. Download the Source Code

This article provided an overview and guide to using the Spring RestTestClient.

Download
You can download the full source code of this example here: Sign up

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Back to top button