How to test the REST Clients of your Spring Boot Application with @RestClientTest


In this Blogpost, I will show you how to test the REST Clients of your Spring Boot Application with @RestClientTest. We will implement a repository that will fetch its data from the Star Wars API. By using the MockRestServiceServer we are going to mock the real API, to isolate our tests and fake inputs for our REST client to test its behavior. In the last part, I will show you how you can isolate the individual tests from another.

What is @RestClientTest?

@RestClientTest is another one of Spring Boot’s annotations used to test a specific slice of your application. As the name suggests, you can use it to test the REST clients inside your application. Applied to a test class it will disable the full-auto-configuration. However, it will auto-configure the support for Jackson, GSON, Jsonb, a RestTemplateBuilder, and a MockRestServiceServer.

To use @RestClientTest, we need to include the dependency org.springframework.boot:spring-boot-starter-test in our project.

pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
...

The Example

Let’s assume we are working on a project that processes that data of Star Wars characters. To access the data of the Star Wars API we are going to build a RestTemplate based REST client. This Client should mimic a Spring Data Repository by providing a similar API to the application. It will be responsible to call the external API and transform the response into a model of our application.

Here is the code of our REST client:

StarWarsCharacterRepository.java
@Repository
public class StarWarsCharacterRepository {

    private final RestTemplate restTemplate;

    public StarWarsCharacterRepository(RestTemplateBuilder restTemplateBuilder, @Value("${swapi.baseUrl}") String baseUrl) {
        this.restTemplate = restTemplateBuilder.rootUri(baseUrl).build();
    }

    public Optional<Character> findById(Long id) {

        ResponseEntity<Character> response;
        try {
            response = restTemplate.getForEntity("/people/{id}", Character.class, id);
        } catch (HttpStatusCodeException e) {
            return Optional.empty();
        }

        return Optional.ofNullable(response.getBody());
    }
}

We use the injected RestTemplateBuilder and the SWAPI URL to build a RestTemplate inside the constructor of our class. This template is then used inside the findById() method to make a response against the remote API. The logic inside this method is quite clear. It will:

  • return an empty Optional if there was any non 200 (OK) response
  • transform the response of the API into a model of our application (Character) and return it

Mocking The REST API

Unit tests should run in isolation, without depending on any other component. Following this dogma, we need to get rid of the real API while running our tests. If the SWAPI servers are slow or not reachable at all, our tests will be slow and unpredictable as well. Like in other types of unit tests we can solve this problem by mocking our dependencies away. To do exactly this, Spring Boot offers us a really handy tool – the MockRestServiceServer. While testing, all requests will be sent to the MockRestServiceServer instead of the real API. This gives us some advantages:

  • we can set expectations about the requests that are sent to the API
  • we can mock responses that will be sent back to our REST client
  • our tests are independent of the real API
  • our tests will execute faster because no HTTP traffic gets sent over the network
  • our tests are predictable and reproducible

Here is an example of a basic mock:

StarWarsCharacterRepositoryTest.java
mockRestServiceServer
    .expect(requestTo("/hello"))
    .andRespond(withSuccess("Hello World", MediaType.TEXT_PLAIN));

With this code, we tell the MockRestServiceServer to expect a request to the URI “/hello” and that it shall respond with the plain text “Hello World”.

Writing the Test

Now let’s start writing the test for our REST client. Here is the basic structure:

StarWarsCharacterRepositoryTest.java
@RestClientTest({StarWarsCharacterRepository.class})
class StarWarsCharacterRepositoryTest {

    @Autowired
    private MockRestServiceServer mockRestServiceServer;
    
    @Autowired
    private StarWarsCharacterRepository starWarsCharacterRepository;

    // tests ...
}

In the first line, you can see that we annotated our test class with @RestClientTest. As mentioned above, this deactivates the full-auto-configuration and instead configures only a slice of our application. Unlike other test slice annotations (like @DataJpaTest, @DataMongoTest, or @JsonTest, we have to pass the information, which class we want to test. In our case, this is the StarWarsCharacterRepository. The next thing we do is to inject a MockRestServiceServer and the REST client we are going to test.

Now let’s add the first test:

StarWarsCharacterRepositoryTest.java
@Test
void findById_ReturnsTheCharacter() {

    final var idLuke = 1L;
    final var lukeJson = new ClassPathResource("luke.json");

    mockRestServiceServer
            .expect(requestTo("/people/" + idLuke))
            .andRespond(withSuccess(lukeJson, MediaType.APPLICATION_JSON));

    var character = starWarsCharacterRepository.findById(idLuke);

    assertTrue(character.isPresent());
    assertEquals("Luke Skywalker", character.get().name());
    assertEquals("19BBY", character.get().birthYear());
    assertEquals("blue", character.get().eyeColor());
    assertEquals("male", character.get().gender());
    assertEquals(77, character.get().mass());
    assertEquals(172, character.get().height());
}

In this test, we verify that the response of the API gets mapped into our Character model. In preparation for this test, we added the file “luke.json” to our test resources. It contains the exact SWAPI payload for Luke Skywalker. To decouple the REST client from the real API we use the MockRestServiceServer to mock a response. We expect a request to “/people/1” and respond with the JSON from our luke.json file. Then we call our REST client and make assertions about the response.

The next thing we want to test is the handling of a non 200 (OK) response from the API. Our test code looks like this:

StarWarsCharacterRepositoryTest.java
@Test
void findById_WithIdOfUnknownCharacter_ReturnsEmptyOptional() {

    final var idUnknownCharacter = 999999L;

    mockRestServiceServer
            .expect(requestTo("/people/" + idUnknownCharacter))
            .andRespond(withStatus(HttpStatus.NOT_FOUND));

    var character = starWarsCharacterRepository.findById(idUnknownCharacter);

    assertTrue(character.isEmpty());
}

As you can see, the test is not that different from the previous one. We use the MockRestServiceServer to mock a 404 (NOT FOUND) response and expect an empty Optional from our StarWarsCharacterRepository.

Teardown after the Test

Like in all kinds of automated tests, we should make sure that our tests do not influence one another. The shared resource between all tests is the MockRestServiceServer and thus should be reset after each test. Otherwise, the mocked behavior from one test could influence the execution of another test which would lead to flakiness. To reset the MockRestServiceServer we can simply call its reset() method.

StarWarsCharacterRepositoryTest.java
@AfterEach
void resetMockServer(){
    mockRestServiceServer.reset();
}

I prefer to do this in an @AfterEach annotated method. This way it can not be forgotten if the test class is extended with another test.

Resources