Writing Tests for REST Clients With JUnit 5
After we have finished this webinar, we:
- Understand how we can auto-configure the system under test with Spring Boot.
- Can mock and stub HTTP requests by using the
MockRestServiceServerclass. - Know how we can write tests for REST clients with JUnit 5.
This webinar assumes that:
Introduction to System Under Test
During this webinar we will write unit tests for a service that manages Message objects. A Message object contains the id and text of a single message. The source code of the Message class looks as follows:
1public class Message {
2
3 private Long id;
4 private String text;
5
6 public Message() {}
7
8 //Getters and setters are omitted
9}
The MessageService class manages messages by using a remote REST API. It has two dependencies which are injected by using constructor injection:
- It uses a
RestTemplateBuilderobject for creating a newRestTemplateobject which sends HTTP requests to the remote REST API. - It uses a
MessageApiUrlBuilderobject for building urls which are used to communicate with the REST API.
This class has two methods:
- The
create()method creates a new message by sending aPOSTrequest to the REST API. This method returns the information of the created message. - The
findById()methods finds the information of the requested message by sending aGETrequest to the REST API. This method returns anOptionalobject that contains the found message. If no message is found, this method returns an emptyOptional.
The source code of the MessageService class looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.boot.web.client.RestTemplateBuilder;
3import org.springframework.http.HttpStatus;
4import org.springframework.stereotype.Service;
5import org.springframework.web.client.HttpClientErrorException;
6import org.springframework.web.client.RestTemplate;
7
8import java.util.Optional;
9
10@Service
11public class MessageService {
12
13 private final RestTemplate restTemplate;
14 private final MessageApiUrlBuilder urlBuilder;
15
16 @Autowired
17 MessageService(RestTemplateBuilder restTemplateBuilder,
18 MessageApiUrlBuilder urlBuilder) {
19 this.restTemplate = restTemplateBuilder.build();
20 this.urlBuilder = urlBuilder;
21 }
22
23 public Message create(String messageText) {
24 Message input = new Message();
25 input.setText(messageText);
26
27 String url = urlBuilder.buildCreateMessageUrl();
28 return restTemplate.postForObject(url, input, Message.class);
29 }
30
31 public Optional<Message> findById(Long id) {
32 String url = urlBuilder.buildFindMessageUrl(id);
33
34 try {
35 Message message = restTemplate.getForObject(url, Message.class);
36 return Optional.of(message);
37 }
38 catch (HttpClientErrorException ex) {
39 if (ex.getStatusCode() == HttpStatus.NOT_FOUND) {
40 return Optional.empty();
41 }
42 throw ex;
43 }
44 }
45}
As we remember, the MessageApiBuilder class is responsible for creating the urls which we use when we communicate with the remote REST API. I like to use url builder classes when I have to communicate with remote APIs because I can read the base url from a properties file and inject it into my url builder class by using constructor injection. This allows me to use different base urls in different environments.
Also, this class has two methods which build the url addresses which we use when we create new messages and find the information of a single message. The source code of the MessageApiBuilder class looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.beans.factory.annotation.Value;
3import org.springframework.stereotype.Component;
4import org.springframework.web.util.UriComponentsBuilder;
5
6@Component
7class MessageApiUrlBuilder {
8
9 private final String apiBaseUrl;
10
11 @Autowired
12 MessageApiUrlBuilder(@Value("${message.api.base.url}") String apiBaseUrl) {
13 this.apiBaseUrl = apiBaseUrl;
14 }
15
16 String buildCreateMessageUrl() {
17 return UriComponentsBuilder.fromUriString(apiBaseUrl)
18 .path("/api/message")
19 .build()
20 .toUriString();
21 }
22
23 String buildFindMessageUrl(Long id) {
24 return UriComponentsBuilder.fromUriString(apiBaseUrl)
25 .path("/api/message/{id}")
26 .buildAndExpand(id)
27 .toUriString();
28 }
29}
Next, we will find out how we can configure the system under test by using Spring Boot auto-configuration.
Configuring the System Under Test
We can configure the system under test by following these steps:
- Integrate the Spring TestContext framework with JUnit 5.
- Annotate the test class with
@RestClientTestannotation and specify the tested components which are added to loaded application context (MessageApiUrlBuilderandMessageService). - Configure the active Spring profile. This allows us to load the base url of the remote REST API from a test specific properties file.
- Add a
MessageServicefield to our test class and ensure that the value of this field is provided by the Spring container. - Add a
MockRestServiceServerfield to our test class and ensure that the value of this field is provided by the Spring container. We will use this field when we want stub and mock HTTP requests send to the REST API.
After we have configured the system under specification, the source code of our test class looks as follows:
1import org.junit.jupiter.api.Tag;
2import org.junit.jupiter.api.extension.ExtendWith;
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
5import org.springframework.test.context.ActiveProfiles;
6import org.springframework.test.context.junit.jupiter.SpringExtension;
7import org.springframework.test.web.client.MockRestServiceServer;
8
9@ExtendWith(SpringExtension.class)
10@RestClientTest({MessageApiUrlBuilder.class, MessageService.class})
11@ActiveProfiles(Profiles.UNIT_TEST)
12@Tag(TestTags.UNIT_TEST)
13class MessageServiceTest {
14
15 @Autowired
16 private MessageService service;
17
18 @Autowired
19 private MockRestServiceServer server;
20}
There are two things I want to point out:
- By default, the
@RestClientTestauto-configures Jackson, GSON, and Jsonb support. It also configures theRestTemplateBuilderandMockRestServiceServerbeans. - As you probably noticed, I added this test class to my unit test suite because it has no external dependencies. In other words, I think that it makes sense to use a larger unit in this situation because it allows us to write more meaningful unit tests.
Additional Reading:
We have now configured the system under test by using the auto-configuration provided by Spring Boot. However, we still have to do one more thing before we can write unit tests for our REST client. We have to create a setup method which resets the state of the mock server before a test method is run. We have to do this because otherwise we cannot specify new expectations after we have send an HTTP request to the mock server.
After we have created this setup method, the source code of our test class looks as follows:
1import org.junit.jupiter.api.BeforeEach;
2import org.junit.jupiter.api.Tag;
3import org.junit.jupiter.api.extension.ExtendWith;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
6import org.springframework.test.context.ActiveProfiles;
7import org.springframework.test.context.junit.jupiter.SpringExtension;
8import org.springframework.test.owweb.client.MockRestServiceServer;
9
10@ExtendWith(SpringExtension.class)
11@RestClientTest({MessageApiUrlBuilder.class, MessageService.class})
12@ActiveProfiles(Profiles.UNIT_TEST)
13@Tag(TestTags.UNIT_TEST)
14class MessageServiceTest {
15
16 @Autowired
17 private MessageService service;
18
19 @Autowired
20 private MockRestServiceServer server;
21
22 @BeforeEach
23 void clearExpectations() {
24 server.reset();
25 }
26}
Let’s move on and find out how we can stub REST API calls with MockRestServiceServer.
Stubbing REST API Calls
When we want to configure the response that is returned when we invoke the remote REST API, we have to follow these rules:
- We have to identify the expected HTTP request by invoking the
expect()method of theMockRestServiceServerclass. This method takes aRequestMatcherobject as a method parameter and returns aResponseActionsobject. - The
RequestMatcherobject determines if our expectation is met. We can create newRequestMatcherobjects by using thestaticfactory methods provided by theMockRestRequestMatchersclass. - The
ResponseActionsobject has two methods which allow us configure more expectations and specify the returned HTTP response. If we want to specify more expectations, we have to invoke itsandExpect()method. On the other hand, if we want to configure the returned HTTP response, we have to invoke theandRespond()method of theResponseActionsinterface. This method takes aResponseCreatorobject as a method parameter. - We can create new
ResponseCreatorobjects by using thestaticfactory methods provided by theMockRestResponseCreatorsclass.
Let’s put our new skills into practice and write some unit tests for the findById() method of the MessageService class. We can write these tests by following these steps:
First, we have to create the nested class which contains our unit tests. After we have created this nested class, our test class looks as follows:
1import org.junit.jupiter.api.BeforeEach;
2import org.junit.jupiter.api.DisplayName;
3import org.junit.jupiter.api.Nested;
4import org.junit.jupiter.api.Tag;
5import org.junit.jupiter.api.extension.ExtendWith;
6import org.springframework.beans.factory.annotation.Autowired;
7import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
8import org.springframework.context.annotation.Import;
9import org.springframework.test.context.ActiveProfiles;
10import org.springframework.test.context.junit.jupiter.SpringExtension;
11import org.springframework.test.web.client.MockRestServiceServer;
12
13@ExtendWith(SpringExtension.class)
14@Import(MessageApiUrlBuilder.class)
15@RestClientTest(MessageService.class)
16@ActiveProfiles(Profiles.UNIT_TEST)
17@Tag(TestTags.UNIT_TEST)
18class MessageServiceTest {
19
20 @Autowired
21 private MessageService service;
22
23 @Autowired
24 private MockRestServiceServer server;
25
26 @BeforeEach
27 void clearExpectations() {
28 server.reset();
29 }
30
31 @Nested
32 @DisplayName("Find a single message")
33 class FindById {
34
35 }
36}
Second, we have to ensure that system under test is working as expected when the requested message is not found. We can do this by following these steps:
- Create the nested test class which contains our unit tests.
- Ensure that the
MockRestServiceServerobject returns the HTTP status code 404 when we try to get the information of the requested message. - Verify that the
findById()method returns an emptyOptionalobject.
After we have written the required unit test, the source code of our test class looks as follows:
1import org.junit.jupiter.api.*;
2import org.junit.jupiter.api.extension.ExtendWith;
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
5import org.springframework.context.annotation.Import;
6import org.springframework.http.HttpStatus;
7import org.springframework.test.context.ActiveProfiles;
8import org.springframework.test.context.junit.jupiter.SpringExtension;
9import org.springframework.test.web.client.MockRestServiceServer;
10
11import java.util.Optional;
12
13import static org.assertj.core.api.Assertions.assertThat;
14import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
15import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
16
17@ExtendWith(SpringExtension.class)
18@Import(MessageApiUrlBuilder.class)
19@RestClientTest(MessageService.class)
20@ActiveProfiles(Profiles.UNIT_TEST)
21@Tag(TestTags.UNIT_TEST)
22class MessageServiceTest {
23
24 @Autowired
25 private MessageService service;
26
27 @Autowired
28 private MockRestServiceServer server;
29
30 @BeforeEach
31 void clearExpectations() {
32 server.reset();
33 }
34
35 @Nested
36 @DisplayName("Find a single message")
37 class FindById {
38
39 @Nested
40 @DisplayName("When the message is not found")
41 class WhenMessageIsNotFound {
42
43 @BeforeEach
44 void returnHttpStatusCodeNotFound() {
45 server.expect(requestTo("http://localhost:9090/api/message/99"))
46 .andRespond(withStatus(HttpStatus.NOT_FOUND));
47 }
48
49 @Test
50 @DisplayName("Should return an empty optional")
51 void shouldReturnEmptyOptional() {
52 Optional<Message> messageResult = service.findById(99L);
53 assertThat(messageResult).isEmpty();
54 }
55 }
56 }
57}
Third, we have to ensure that system under test is working as expected when the requested message is found. We can do this by following these steps:
- Create the nested test class which contains our unit tests.
- Ensure that the
MockRestServiceServerobject returns the information of the requested message. - Verify that the system under test returns a
Messagethat has the correct id. - Ensure that the system under test returns a
Messagethat has the correct message text.
After we have written the required unit tests, the source code of our test class looks as follows:
1import org.junit.jupiter.api.*;
2import org.junit.jupiter.api.extension.ExtendWith;
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
5import org.springframework.context.annotation.Import;
6import org.springframework.http.HttpStatus;
7import org.springframework.http.MediaType;
8import org.springframework.test.context.ActiveProfiles;
9import org.springframework.test.context.junit.jupiter.SpringExtension;
10import org.springframework.test.web.client.MockRestServiceServer;
11
12import java.util.Optional;
13
14import static org.assertj.core.api.Assertions.assertThat;
15import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
16import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
17import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
18
19@ExtendWith(SpringExtension.class)
20@Import(MessageApiUrlBuilder.class)
21@RestClientTest(MessageService.class)
22@ActiveProfiles(Profiles.UNIT_TEST)
23@Tag(TestTags.UNIT_TEST)
24class MessageServiceTest {
25
26 @Autowired
27 private MessageService service;
28
29 @Autowired
30 private MockRestServiceServer server;
31
32 @BeforeEach
33 void clearExpectations() {
34 server.reset();
35 }
36
37 @Nested
38 @DisplayName("Find a single message")
39 class FindById {
40
41 @Nested
42 @DisplayName("When the message is not found")
43 class WhenMessageIsNotFound {
44
45 @BeforeEach
46 void returnHttpStatusCodeNotFound() {
47 server.expect(requestTo("http://localhost:9090/api/message/99"))
48 .andRespond(withStatus(HttpStatus.NOT_FOUND));
49 }
50
51 @Test
52 @DisplayName("Should return an empty optional")
53 void shouldReturnEmptyOptional() {
54 Optional<Message> messageResult = service.findById(99L);
55 assertThat(messageResult).isEmpty();
56 }
57 }
58
59 @Nested
60 @DisplayName("When the message is found")
61 class WhenMessageIsFound {
62
63 @BeforeEach
64 void returnFoundMessage() {
65 server.expect(requestTo("http://localhost:9090/api/message/99"))
66 .andRespond(withSuccess("{" +
67 "\"id\": 99," +
68 "\"text\": \"Hello World!\"" +
69 "}",
70 MediaType.APPLICATION_JSON_UTF8)
71 );
72 }
73
74 @Test
75 @DisplayName("Should return a message that has the correct id")
76 void shouldReturnMessageWithCorrectId() {
77 Message found = service.findById(99L).get();
78 assertThat(found.getId()).isEqualTo(99L);
79 }
80
81 @Test
82 @DisplayName("Should return a message that has the correct text")
83 void shouldReturnMessageWithCorrectText() {
84 Message found = service.findById(99L).get();
85 assertThat(found.getText()).isEqualTo("Hello World!");
86 }
87 }
88 }
89}
Additional Reading:
We can now write unit tests for methods which fetch information from a remote REST API. Next, we will find out how we can mock REST API calls by using the MockRestServiceServer class.
Mocking REST API Calls
When we specify our expectations by invoking the expect() method of the MockRestServiceServer class, we also ensure that our test fail if our expectations are not met. In other words, if we send an unexpected HTTP request to the MockRestServiceServer, our test will fail. This means that we cannot really implement “proper” stubbing by using the approach described on this webinar.
On the other hand, if we want to verify that the expected HTTP request is send X times, we have to follow these rules:
- We can specify the expected invocation count by passing a
ExpectedCountobject as the first method parameter of theexpect()method of theMockRestServiceServerclass. If we omit this method parameter, theMockRestServiceServerassumes that we expect that the expected HTTP request is send only once. - We can create new
ExpectedCountobjects by using the static factory methods provided by theExpectedCountclass. - We have to invoke the system under test and verify that the expected HTTP request was send X times. We can do this by invoking the
verify()method of theMockRestServiceServerclass.
Let’s see how we can ensure that the create() method of the MessageService class creates only one message by using the correct information and returns the information of the created message. We can write the required tests by following these steps:
First, we have to create the nested class which contains our unit tests. After we have created this nested class, our test class looks as follows:
1import org.junit.jupiter.api.BeforeEach;
2import org.junit.jupiter.api.DisplayName;
3import org.junit.jupiter.api.Nested;
4import org.junit.jupiter.api.Tag;
5import org.junit.jupiter.api.extension.ExtendWith;
6import org.springframework.beans.factory.annotation.Autowired;
7import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
8import org.springframework.test.context.ActiveProfiles;
9import org.springframework.test.context.junit.jupiter.SpringExtension;
10import org.springframework.test.web.client.MockRestServiceServer;
11
12@ExtendWith(SpringExtension.class)
13@RestClientTest({MessageApiUrlBuilder.class, MessageService.class})
14@ActiveProfiles(Profiles.UNIT_TEST)
15@Tag(TestTags.UNIT_TEST)
16class MessageServiceTest {
17
18 @Autowired
19 private MessageService service;
20
21 @Autowired
22 private MockRestServiceServer server;
23
24 @BeforeEach
25 void clearExpectations() {
26 server.reset();
27 }
28
29 @Nested
30 @DisplayName("Create a new message")
31 class CreateMessage {
32
33 }
34}
Second, we have to ensure that system under test is working as expected when we create a new message. We can do this by following these steps:
- Ensure that the
MockRestServiceServerobject expects to receive onePOSTrequest that contains the correct information and ensure that it returns the information of the created message when it receives this request. - Verify that the system under test creates one message.
- Ensure that the system under test returns a
Messagethat has the correct id. - Verify that the system under test returns a
Messagethat has the correct message text.
After we have written the required unit tests, the source code of our test class looks as follows:
1import org.junit.jupiter.api.*;
2import org.junit.jupiter.api.extension.ExtendWith;
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
5import org.springframework.http.HttpMethod;
6import org.springframework.http.MediaType;
7import org.springframework.test.context.ActiveProfiles;
8import org.springframework.test.context.junit.jupiter.SpringExtension;
9import org.springframework.test.web.client.MockRestServiceServer;
10
11import static org.assertj.core.api.Assertions.assertThat;
12import static org.hamcrest.Matchers.is;
13import static org.hamcrest.Matchers.nullValue;
14import static org.springframework.test.web.client.ExpectedCount.times;
15import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
16import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
17
18@ExtendWith(SpringExtension.class)
19@RestClientTest({MessageApiUrlBuilder.class, MessageService.class})
20@ActiveProfiles(Profiles.UNIT_TEST)
21@Tag(TestTags.UNIT_TEST)
22class MessageServiceTest {
23
24 @Autowired
25 private MessageService service;
26
27 @Autowired
28 private MockRestServiceServer server;
29
30 @BeforeEach
31 void clearExpectations() {
32 server.reset();
33 }
34
35 @Nested
36 @DisplayName("Create a new message")
37 class CreateMessage {
38
39 @BeforeEach
40 void returnCreatedMessageAsJson() {
41 server.expect(times(1), method(HttpMethod.POST))
42 .andExpect(requestTo("http://localhost:9090/api/message"))
43 .andExpect(header("Content-Type", MediaType.APPLICATION_JSON_UTF8_VALUE))
44 .andExpect(jsonPath("$.id", nullValue()))
45 .andExpect(jsonPath("$.text", is("Hello World!")))
46 .andRespond(withSuccess("{" +
47 "\"id\": 99," +
48 "\"text\": \"Hello World!\"" +
49 "}",
50 MediaType.APPLICATION_JSON_UTF8)
51 );
52 }
53
54 @Test
55 @DisplayName("Should create a new message by invoking the remote API once")
56 void shouldCreateNewMessageByInvokingRemoteApiOnce() {
57 service.create("Hello World!");
58 server.verify();
59 }
60
61 @Test
62 @DisplayName("Should return a message that has the correct id")
63 void shouldReturnMessageWithCorrectId() {
64 Message created = service.create("Hello World!");
65 assertThat(created.getId()).isEqualTo(99L);
66 }
67
68 @Test
69 @DisplayName("Should return a message that has the correct text")
70 void shouldReturnMessageWithCorrectText() {
71 Message created = service.create("Hello World!");
72 assertThat(created.getText()).isEqualTo("Hello World!");
73 }
74 }
75}
Unfortunately, if we use this approach, we have to add the JUnit 4 jar to classpath even if we want to use JUnit 5. If we don’t do this, our tests will fail because the org.junit.Assert class is not found. This is really irritating because we have to always ensure that our tests use the right @Test annotation.
Additional Reading:
We can now write unit tests for REST clients with Spring Boot and JUnit 5. Let’s summarize what we learned from this webinar.
Summary
This webinar has taught us three things:
- We can auto-configure the system under test by using the
@RestClientTestannotation. - We can mock and stub HTTP requests by using the
MockRestServiceServerclass. - We have to remember to reset the state of the mock server before a test method is run.