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 MockRestServiceServer class.
  • Know how we can write tests for REST clients with JUnit 5.

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 RestTemplateBuilder object for creating a new RestTemplate object which sends HTTP requests to the remote REST API.
  • It uses a MessageApiUrlBuilder object 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 a POST request 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 a GET request to the REST API. This method returns an Optional object that contains the found message. If no message is found, this method returns an empty Optional.

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:

  1. Integrate the Spring TestContext framework with JUnit 5.
  2. Annotate the test class with @RestClientTest annotation and specify the tested components which are added to loaded application context (MessageApiUrlBuilder and MessageService).
  3. Configure the active Spring profile. This allows us to load the base url of the remote REST API from a test specific properties file.
  4. Add a MessageService field to our test class and ensure that the value of this field is provided by the Spring container.
  5. Add a MockRestServiceServer field 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 @RestClientTest auto-configures Jackson, GSON, and Jsonb support. It also configures the RestTemplateBuilder and MockRestServiceServer beans.
  • 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 the MockRestServiceServer class. This method takes a RequestMatcher object as a method parameter and returns a ResponseActions object.
  • The RequestMatcher object determines if our expectation is met. We can create new RequestMatcher objects by using the static factory methods provided by the MockRestRequestMatchers class.
  • The ResponseActions object 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 its andExpect() method. On the other hand, if we want to configure the returned HTTP response, we have to invoke the andRespond() method of the ResponseActions interface. This method takes a ResponseCreator object as a method parameter.
  • We can create new ResponseCreator objects by using the static factory methods provided by the MockRestResponseCreators class.

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:

  1. Create the nested test class which contains our unit tests.
  2. Ensure that the MockRestServiceServer object returns the HTTP status code 404 when we try to get the information of the requested message.
  3. Verify that the findById() method returns an empty Optional object.

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:

  1. Create the nested test class which contains our unit tests.
  2. Ensure that the MockRestServiceServer object returns the information of the requested message.
  3. Verify that the system under test returns a Message that has the correct id.
  4. Ensure that the system under test returns a Message that 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}

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 ExpectedCount object as the first method parameter of the expect() method of the MockRestServiceServer class. If we omit this method parameter, the MockRestServiceServer assumes that we expect that the expected HTTP request is send only once.
  • We can create new ExpectedCount objects by using the static factory methods provided by the ExpectedCount class.
  • 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 the MockRestServiceServer class.

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:

  1. Ensure that the MockRestServiceServer object expects to receive one POST request that contains the correct information and ensure that it returns the information of the created message when it receives this request.
  2. Verify that the system under test creates one message.
  3. Ensure that the system under test returns a Message that has the correct id.
  4. Verify that the system under test returns a Message that 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 @RestClientTest annotation.
  • We can mock and stub HTTP requests by using the MockRestServiceServer class.
  • We have to remember to reset the state of the mock server before a test method is run.

Get the source code from Github