A Developer’s Guide to Testing Spring REST Clients with @RestClientTest

In modern microservices architecture, it’s rare for a service to live in complete isolation. Most applications need to communicate with other services over the network, typically via REST APIs. When you build a component that consumes an external REST API, a critical question arises: how do you test it reliably without actually making network calls to a live, and potentially unstable, external service?

This is where Spring Boot’s test slices come to the rescue. For testing your REST clients, the framework provides a powerful and elegant solution: the @RestClientTest annotation. Let’s dive deep into how you can use it to write clean, fast, and reliable tests for your HTTP client components.

What Exactly is @RestClientTest?

@RestClientTest is a “test slice” annotation specifically designed to test REST client components. Instead of loading your entire Spring application context (like @SpringBootTest does), it focuses only on the beans relevant to REST client operations. This makes your tests significantly faster and less prone to side effects from unrelated configurations.

When you use @RestClientTest, Spring Boot will auto-configure the following for you:

  • The Client Under Test: The specific REST client bean you want to test.
  • MockRestServiceServer: A bean that lets you mock the server-side responses. You can instruct it: “When my client calls /api/employees/1, respond with this specific JSON.”
  • RestTemplateBuilder: Used to help construct RestTemplate instances.
  • Jackson/Gson Support: It automatically includes support for serializing and deserializing JSON, so you can test your client-side data mapping.

In short, it provides the perfect, minimal environment to verify that your client builds the correct HTTP request and correctly parses the HTTP response — all without a single packet leaving your machine.

A Practical Example: Testing an Employee API Client

Let’s walk through a concrete example. Imagine we are building a service that needs to fetch employee details from an external HR system’s API.

Step 1: The Data Model (DTO)

First, let’s define a simple Java record to hold the employee data we expect from the API. Records are ideal for immutable Data Transfer Objects (DTOs).

public record Employee(
    Integer id,
    String firstName,
    String lastName,
    String email
) {}

Step 2: The REST Client Service

Next, we’ll create our client service using Spring’s modern, fluent RestClient interface — the recommended approach for new applications. This service exposes a single method to fetch an employee by their ID.

import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;

@Service
public class EmployeeClient {

    private final RestClient restClient;

    // Inject a pre-configured RestClient bean (with base URL set in configuration)
    public EmployeeClient(RestClient restClient) {
        this.restClient = restClient;
    }

    public Employee getEmployeeById(Integer id) {
        return restClient.get()
            .uri("/employees/{id}", id)
            .retrieve()
            .body(Employee.class);
    }
}

Note: You would typically define a RestClient bean pointing to a base URL from your application properties. For example:

@Configuration
public class AppConfig {

    @Bean
    public RestClient employeeRestClient(@Value("${employee.api.base-url}") String baseUrl) {
        return RestClient.builder()
            .baseUrl(baseUrl)
            .build();
    }
}

Step 3: Writing the Test with @RestClientTest

Now for the main event! Let’s create our test class using JUnit 5 and AssertJ for fluent assertions.

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

// 1. Annotate with @RestClientTest, specifying the component to test
@RestClientTest(EmployeeClient.class)
class EmployeeClientTest {

    // 2. Inject MockRestServiceServer and the client under test
    @Autowired
    private MockRestServiceServer mockServer;

    @Autowired
    private EmployeeClient employeeClient;

    // 3. Inject ObjectMapper to easily create JSON strings from objects
    @Autowired
    private ObjectMapper objectMapper;

    @Test
    void whenGetEmployeeById_thenReturnsEmployee() throws Exception {
        // Arrange: Prepare mock data and expected JSON response
        Employee mockEmployee = new Employee(1, "Ankur", "M", "[email protected]");
        String employeeJson = objectMapper.writeValueAsString(mockEmployee);

        // 4. Configure MockRestServiceServer expectations
        mockServer.expect(requestTo("/employees/1"))
                  .andRespond(withSuccess(employeeJson, MediaType.APPLICATION_JSON));

        // Act: Call the method on the client
        Employee result = employeeClient.getEmployeeById(1);

        // Assert: Verify the response was correctly deserialized
        assertThat(result).isNotNull();
        assertThat(result.id()).isEqualTo(1);
        assertThat(result.firstName()).isEqualTo("Ankur");

        // 5. Verify all expected requests were actually made
        mockServer.verify();
    }
}

Breaking Down the Test

  1. @RestClientTest(EmployeeClient.class): Tells Spring Boot to set up a minimal test context for the EmployeeClient bean only.
  2. Autowired beans: We inject both our EmployeeClient (the bean under test) and the MockRestServiceServer auto-configured by Spring.
  3. ObjectMapper: Injected to serialize our Employee object to JSON — avoiding brittle manual string manipulation.
  4. Arrange and Mock: mockServer.expect(requestTo("/employees/1")) declares the expected request path. .andRespond(withSuccess(...)) provides the 200 OK response with a JSON body and correct Content-Type.
  5. Act and Assert: The client is invoked; MockRestServiceServer intercepts the call and returns our mock response. We then assert correct deserialization of the JSON into an Employee object.
  6. mockServer.verify(): Confirms the expected request was actually made. If the client called a different URL or made no call at all, this fails the test — a critical safety net.

What if I’m Still Using RestTemplate?

@RestClientTest was originally designed for RestTemplate and works seamlessly with it. If your EmployeeClient uses a RestTemplate, the client changes slightly but the test remains almost identical:

// Alternative implementation using RestTemplate
@Service
public class EmployeeClient {

    private final RestTemplate restTemplate;

    public EmployeeClient(RestTemplateBuilder builder) {
        // Base URL is typically externalized to application properties
        this.restTemplate = builder.rootUri("http://hr-api.example.com").build();
    }

    public Employee getEmployeeById(Integer id) {
        return restTemplate.getForObject("/employees/{id}", Employee.class, id);
    }
}

@RestClientTest auto-configures a RestTemplateBuilder so that any RestTemplate built from it is wired through the MockRestServiceServer. The mocking logic with mockServer.expect(...).andRespond(...) stays exactly the same — a significant benefit when migrating legacy code.

Testing Error Scenarios

A well-rounded test suite should also cover failure paths — network errors, 404 responses, and 500 errors. MockRestServiceServer makes this straightforward:

import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
import org.springframework.http.HttpStatus;

@Test
void whenEmployeeNotFound_thenThrowsException() {
    // Simulate a 404 Not Found response from the server
    mockServer.expect(requestTo("/employees/99"))
              .andRespond(withStatus(HttpStatus.NOT_FOUND));

    // Assert that your client throws the appropriate exception
    assertThatThrownBy(() -> employeeClient.getEmployeeById(99))
        .isInstanceOf(HttpClientErrorException.NotFound.class);

    mockServer.verify();
}

Conclusion

The @RestClientTest annotation is an indispensable tool for any Spring developer building services that interact with external APIs. It allows you to write focused, fast, and highly reliable tests for your client-side logic — without the complexity and flakiness of live network dependencies.

By isolating your client component and mocking the server with MockRestServiceServer, you can confidently verify that your code correctly builds requests and parses responses, leading to more robust and maintainable applications. The next time you write a REST client in Spring, reach for @RestClientTest — your future self will thank you.

Leave a Reply

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