While watching Andy Wilkinson's great talk, Testing Spring Boot Applications on YouTube, he brought my attention to a hidden gem for testing the Spring RestTemplate. In the past, I always wondered how to write proper tests for client classes using the RestTemplate
to fetch data from external services. In his presentation, he mentioned the @RestClientTest
annotation that provides everything you need to test such classes. Compared to WireMock for testing our RestTemplate
in isolation, this solution requires less setup as everything is part of Spring Boot.
With this blog post, I'll walk you through a Spring Boot 2.4 application using the @RestClientTest
annotation.
Spring RestTemplate Project Setup
The application is a usual Tomcat-based Spring Boot Web MVC application. The RestTemplate
is used to fetch data from a remote API.
Besides, we're using Java 16. This Java version is optional and not a must-have. However, we'll benefit from Java's new text block feature when preparing JSON strings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>de.rieckpil.blog</groupId> <artifactId>testing-spring-rest-template</artifactId> <version>0.0.1-SNAPSHOT</version> <name>testing-spring-rest-template</name> <description>Demo project for Spring Boot</description> <properties> <java.version>16</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Spring Boot Maven Plugin --> </plugins> </build> </project> |
That's everything we need. No additional dependency required as the @RestClientTest
annotation is part of the spring-boot-test-autoconfigure
dependency which comes with spring-boot-starter-test
.
Please note that the upcoming test examples are using JUnit Jupiter (part of JUnit 5). Nevertheless, I'll explain what needs to change when using JUnit 4.X.
Inside the @RestClientTest Annotation
Let's first have a look at the @RestClientTest
annotation to understand what's happening behind the scenes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @BootstrapWith(RestClientTestContextBootstrapper.class) @ExtendWith(SpringExtension.class) @OverrideAutoConfiguration(enabled = false) @TypeExcludeFilters(RestClientTypeExcludeFilter.class) @AutoConfigureCache @AutoConfigureWebClient @AutoConfigureMockRestServiceServer @ImportAutoConfiguration public @interface RestClientTest { // ... } |
The annotation is used to combine (a lot of) other annotations mostly used for auto-configuration. Together all of these annotations only bootstrap the required beans to test a specific part of the application.
With @ExtendWith(SpringExtension.class)
the annotation registers Spring's JUnit Jupiter extension. Among other things, we need this annotation to inject objects from the test's ApplicationContext
. For those of you who are using JUnit 4, make sure to always add @RunWith(SpringRunner.class)
to your tests. This is required as the SpringExtension
only works with JUnit Jupiter.
Next @AutoConfigureWebClient
is used to enable and configure the auto-configuration of web clients. By default, Spring Boot configures the RestTemplateBuilder
for us. If we specify @AutoConfigureWebClient(registerRestTemplate=true)
, also concrete RestTemplate
beans are available to inject, which we'll see later on.
The last important annotation is @AutoConfigureMockRestServiceServer
. This ensures a bean of type MockRestServiceServer
is part of the Spring TestContext and ready to inject into our test classes to mock the HTTP response.
Testing Code Using the RestTemplateBuilder
If we've already used the RestTemplate
in the past, the following setup might look familiar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | @Component public class UserClient { private final RestTemplate restTemplate; public UserClient(RestTemplateBuilder restTemplateBuilder) { this.restTemplate = restTemplateBuilder.rootUri("https://reqres.in").build(); } public User getSingleUser(Long id) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity<Void> requestEntity = new HttpEntity<>(headers); return this.restTemplate .exchange("/api/users/{id}", HttpMethod.GET, requestEntity, User.class, id) .getBody(); } } |
Our UserClient
basically returns a POJO from a remote API. For this demo, we're using a dummy API.
Testing this part of your application while mocking the RestTemplateBuilder
results in a lot of setup ceremony. The benefit of just testing such code parts using only Mocktio is questionable. It would be great to include HTTP semantics and test different responses from the remote server.
We can do better than just mocking everything by using the@RestClientTest
. Once we annotate a test class with this annotation, we have to specify which class we want to test,e.g.:@RestClientTest(UserClient.class)
.
Next, we can inject everything we need for our test
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | @RestClientTest(UserClient.class) class UserClientTest { @Autowired private UserClient userClient; @Autowired private ObjectMapper objectMapper; @Autowired private MockRestServiceServer mockRestServiceServer; // ... } |
Given this MockRestSerivceServer
instance, we can now specify the result of this mock server for each test case:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Test void userClientSuccessfullyReturnsUser() { String json = """ { "data": { "id": 1, "email": "janet.weaver@reqres.in", "first_name": "Janet", "last_name": "Weaver", "avatar": "https://s3.amazonaws.com/uifaces/faces/twitter/josephstein/128.jpg" } } """; this.mockRestServiceServer .expect(requestTo("/api/users/1")) .andRespond(withSuccess(json, MediaType.APPLICATION_JSON)); User result = userClient.getSingleUser(1L); assertNotNull(result); } |
The example above is using the Java 16 text block feature to define the JSON string response. Besides the HTTP body, we can also configure the content type and HTTP status of the response.
For those of you, who don't use Java 16 yet, you can also use the injected ObjectMapper
to create a valid JSON string:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @Test void userClientSuccessfullyReturnsUserDuke() throws Exception { String json = this.objectMapper this.mockRestServiceServer .expect(requestTo("/api/users/42")) .andRespond(withSuccess(json, MediaType.APPLICATION_JSON)); User result = userClient.getSingleUser(42L); assertEquals(42L, result.getData().getId()); assertEquals("duke", result.getData().getFirstName()); assertEquals("duke", result.getData().getLastName()); assertEquals("duke", result.getData().getAvatar()); } |
We can now also test how our method behaves in case of a server error in the remote system:
1 2 3 4 5 6 7 | @Test void userClientThrowsExceptionWhenNoUserIsFound() { this.mockRestServiceServer.expect(requestTo("/api/users/1")) .andRespond(MockRestResponseCreators.withStatus(HttpStatus.NOT_FOUND)); assertThrows(HttpClientErrorException.class, () -> userClient.getSingleUser(1L)); } |
The example above propagates the exception, but our client class might handle it differently and return null
or an empty Optional
, which we can easily verify with this setup.
Testing Code Using a Spring RestTemplate Bean
Furthermore, some parts of our application might not directly use the RestTemplateBuilder
. It's also common to prepare different RestTemplate
beans for our application inside a @Configuration
class:
1 2 3 4 5 6 7 8 9 10 11 | @Configuration public class RestTemplateConfig { @Bean public RestTemplate reqresRestTemplate(RestTemplateBuilder restTemplateBuilder) { return restTemplateBuilder.rootUri("https://reqres.in/") .setConnectTimeout(Duration.ofSeconds(2)) .setReadTimeout(Duration.ofSeconds(2)) .build(); } } |
And the client directly injects the configured RestTemplate
:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Component public class ResourceClient { private final RestTemplate reqresRestTemplate; public ResourceClient(RestTemplate reqresRestTemplate) { this.reqresRestTemplate = reqresRestTemplate; } public JsonNode getSingleResource(Long id) { return this.reqresRestTemplate.getForObject("/api/unkown/{id}", JsonNode.class, id); } } |
For such use cases, we have to set up our test class slightly different:
1 2 3 4 5 6 7 | @RestClientTest(ResourceClient.class) @AutoConfigureWebClient(registerRestTemplate = true) class ResourceClientTest { // ... } |
Finally, the MockRestSerivceServer
provides a way to reset all expectations/recorded requests and verify that every expectation was actually used. This is quite helpful and can be integrated using the JUnit test lifecycle:
1 2 3 4 5 6 7 8 9 10 11 | @BeforeEach // @Before for JUnit 4 void setUp() { this.mockRestServiceServer.reset(); } @AfterEach // @After for JUnit 4 void tearDown() { this.mockRestServiceServer.verify(); } |
This ensures there are no side effects between tests.
Unfortunately, we can't use this whole setup if our application already uses the Spring WebClient instead of the RestTemplate
. There is a different approach to have something similar for the WebClient
, which is covered in another article.
Apart from the @RestClientTest
annotation, Spring Boot offers further test slice annotations to verify different parts of our application in isolation:
- Spring Boot Test Slices Overview and Usage
- Guide to Testing Spring Boot Applications With MockMvc
- Test Your Spring Boot JPA Persistence Layer With @DataJpaTest
- MongoDB Testcontainers Setup for @DataMongoTest
The full source code (including all tests) for this Spring Boot application example is available on GitHub.
Have fun testing your Spring RestTemplate,
Phil