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.
<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:
@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:
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:
@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:
@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:
@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.
@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
- You can find the code of this blog post in this GitHub repository.
- You do want to make sure your JSON deserialization works as expected? Have a look at my other post “How to test JSON (de-)serialization in your Spring Boot application with @JsonTest”
- Have a look at Spring Boot’s documentation for more official information about
@RestClientTest