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.
With this blog post, I'll walk you through a Spring Boot 2.2 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.
In addition, I'm using Java 13, which is optional and not required for this to work. It's included to demonstrate how the experimental text block feature benefits for 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 41 42 43 44 45 46 47 | <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</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>13</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> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <!-- optional plugin configuration to use Java 13 preview featues --> </plugins> </build> </project> |
That's everything you 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 example uses JUnit 5, but I'll explain what you have to change if your application is using JUnit 4.X.
Compared to using WireMock for testing your RestTemplate
in isolation, this solution requires less set up as everything is part of Spring Boot.
Inside the @RestClientTest annotation
Let's first have a look at the @RestClientTest
annotation to truly understand what is happening in 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 make sure to only bootstrap required beans to test a specific part of the application.
With @ExtendWith(SpringExtension.class)
the annotation registers the Spring JUnit 5 extension. Among other things we need this annotation to autowire objects from the test's ApplicationContext
. For those of you who are using JUnit 4, make sure to always add @RunWith(SpringRunner.class)
to all your tests. This is required as the SpringExtension
only works with JUnit 5.
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 you'll see later on.
The last important annotation is @AutoConfigureMockRestServiceServer
. This ensures we can inject a bean of type MockRestServiceServer
to our tests and mock the HTTP response.
Testing code using the RestTemplateBuilder
If you have used the RestTemplate
in the past, the following 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(); } } |
It basically returns a POJO from a remote API. For this example, I'm 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 if we can also include HTTP semantics and test different responses from the remote server.
All of this we can solve using the setup we get with@RestClientTest
. Once you annotate a test class with this, you have to specify which class you want to actually test,e.g.:@RestClientTest(UserClient.class)
. Next, you are able to autowire everything you need in the 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 public 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 13 text block preview feature to define the JSON string response. Besides the HTTP body, you are also able to configure the content type and HTTP status of the response.
For those of you, who don't use Java 13 yet, you can also use the injected ObjectMapper
to create a proper JSON string:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @Test public 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()); } |
You can now also test how your method behaves in case of a server error in the remote system:
1 2 3 4 5 6 7 | @Test public void userClientThrowsExceptionWhenNoUserIsFound() { this.mockRestServiceServer.expect(requestTo("/api/users/1")) .andRespond(MockRestResponseCreators.withStatus(HttpStatus.NOT_FOUND)); assertThrows(HttpClientErrorException.class, () -> userClient.getSingleUser(1L)); } |
The example above just propagates the exception, but your client class might handle it differently and return null
or an empty Optional
, which you can test with this.
Testing code using a Spring RestTemplate bean
Furthermore, some parts of your application might not directly use the RestTemplateBuilder
. It's also common to prepare different RestTemplate
beans for your 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, you have to set up your test class like the following:
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 public void setUp() { this.mockRestServiceServer.reset(); } @AfterEach // @After for JUnit 4 public void tearDown() { this.mockRestServiceServer.verify(); } |
This ensures to not have any side effects between tests by creating unnecessary HTTP stubs.
Unfortunately, you can't use this whole setup if your application already uses the Spring WebClient instead of the RestTemplate
. There is a different approach to have something similar for the WebClient
, which I covered in another guide.
The full source code (including all tests) for this Spring Boot application example is available on GitHub.
Have fun testing your Spring RestTemplate,
Phil
[…] >> Testing your Spring RestTemplate with @RestClientTest [rieckpil.de] […]
[…] one of the last blog post, I demonstrated how to test the Spring RestTemplate wWebClientith @RestClientTest. With this elegant solution, you can easily test parts of your application that use the […]