The previous part of my Spring MVC Test tutorial described how we can send HTTP requests to the system under test and write assertions for the response returned by the tested controller method. This blog post describes how we can use the information provided by the previous part of this tutorial when we are writing unit tests for a controller method which returns the information of a single item as JSON.

After we have finished this blog post, we:

  • Know how we can ensure that the system under test returns the correct HTTP status code.
  • Can verify that the system under test returns the correct information.

Let’s begin.

Introduction to the System Under Test

We have to write unit tests for a controller method that processes GET requests send to the path: ‘/todo-item/{id}’. The contract of this API endpoint is described in the following:

  • If the requested todo item is found, the system under test returns the HTTP status code 200. The system under test also creates a JSON document which contains the information of the found todo item and adds this document to the body of the returned HTTP response.
  • If the requested todo item isn’t found, the system under test returns the HTTP status code 404. Because no todo item is found, the body of the returned HTTP response is empty.

The tested controller method is called findById() and it simply returns the information of the todo item that’s found from the database. The source code of the tested controller method looks as follows:

 1import org.springframework.beans.factory.annotation.Autowired;
 2import org.springframework.web.bind.annotation.GetMapping;
 3import org.springframework.web.bind.annotation.PathVariable;
 4import org.springframework.web.bind.annotation.RequestMapping;
 5import org.springframework.web.bind.annotation.RestController;
 6
 7@RestController
 8@RequestMapping("/todo-item")
 9public class TodoItemCrudController {
10    private final TodoItemCrudService service;
11
12    @Autowired
13    public TodoItemCrudController(TodoItemCrudService service) {
14        this.service = service;
15    }
16
17    @GetMapping("{id}")
18    public TodoItemDTO findById(@PathVariable("id") Long id) {
19        return service.findById(id);
20    }
21}

The TodoItemDTO class is a DTO that contains the information of a single todo item. Its source code looks as follows:

 1public class TodoItemDTO {
 2
 3    private Long id;
 4    private String description;
 5    private List<TagDTO> tags;
 6    private String title;
 7    private TodoItemStatus status;
 8
 9    //Getters and setters are omitted
10}

The TagDTO class is a DTO that contains the information of a single tag. Its source code looks as follows:

1public class TagDTO {
2
3    private Long id;
4    private String name;
5
6    //Getters and setters are omitted
7}

The TodoItemStatus enum specifies the possible statuses of a todo item. Its source code looks as follows:

1public enum TodoItemStatus {
2    OPEN,
3    IN_PROGRESS,
4    DONE
5}

For example, if the found todo item is in progress and has one tag, the following JSON document is returned back to the client:

 1{
 2	"id":1,
 3	"description":"Remember to use JUnit 5",
 4	"tags":[
 5		{
 6			"id":9,
 7			"name":"Code"
 8		}
 9	],
10	"title":"Write example application",
11	"status":"IN_PROGRESS"
12}

Next, we will learn how we can write assertions for the response returned by the system under test.

Writing Assertions for the Response Returned by the System Under Test

Before we can write unit tests for a Spring MVC controller which returns the information of a single item as JSON, we have to learn how we can write assertions for the HTTP response returned by the system under test. When we want to write assertions for the HTTP response returned by the tested Spring MVC controller, we have to use these static methods of the MockMvcResultMatchers class:

  • The status() method returns a StatusResultMatchers object which allows us to write assertions for the returned HTTP status.
  • The content() method returns a ContentResultMatchers object which allows us to write assertions for the content of the returned HTTP response.
  • The jsonPath() method returns a JsonPathResultMatchers object which allows us to write assertions for the body of the returned HTTP response by using JsonPath expressions and Hamcrest matchers.

Because we are writing assertions by using JsonPath expressions and Hamcrest matchers, we must ensure that the json-path and hamcrest-library dependencies are found from the classpath. If we are using Maven and Spring Boot dependency management, we can declare these dependencies by adding the following XML snippet to the dependencies section of our POM file:

 1<dependency>
 2    <groupId>org.hamcrest</groupId>
 3    <artifactId>hamcrest-library</artifactId>
 4    <scope>test</scope>
 5</dependency>
 6<dependency>
 7    <groupId>com.jayway.jsonpath</groupId>
 8    <artifactId>json-path</artifactId>
 9    <scope>test</scope>
10</dependency>

Let’s move on and find out how we can write a request builder method which sends GET requests to the system under test.

Writing a New Request Builder Method

Because we want to remove duplicate code from our test class, we have to create and send HTTP requests to the system under test by using a so called request builder class. In other words, before we can write unit tests for the system under test, we have write to a request builder method which creates and sends HTTP requests to the system under test. We can write this request builder method by following these steps:

  1. Add a new method called findById() to our request builder class. Ensure that this method takes the id of the todo item as a method parameter and returns a ResultActions object.
  2. Send a GET request to the path: ‘/todo-item/{id}’ by invoking the perform() method of the MockMvc class. Remember to return the ResultActions object that’s returned by the perform() method.

After we have written our request builder method, the source code of our request builder class looks as follows:

 1import org.springframework.test.web.servlet.MockMvc;
 2import org.springframework.test.web.servlet.ResultActions;
 3
 4import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
 5
 6class TodoItemRequestBuilder {
 7
 8    private final MockMvc mockMvc;
 9
10    TodoItemRequestBuilder(MockMvc mockMvc) {
11        this.mockMvc = mockMvc;
12    }
13
14    ResultActions findById(Long id) throws Exception {
15        return mockMvc.perform(get("/todo-item/{id}", id));
16    }
17}

Next, we will learn to write unit tests for the system under test.

Writing Unit Tests for the System Under Test

When we want to write unit tests for the system under test, we have to follow these steps:

First, we have to add the required class hierarchy to our test class. Because we are writing unit tests, we can create this class hierarchy by following these steps:

  1. Add an inner class called FindById to our test class. This inner class contains the test methods which ensure that the system under test is working as expected.
  2. Add an inner class called WhenRequestedTodoItemIsNotFound to the FindById class. This inner class contains the test methods which ensure that the system under test is working as expected when the requested todo item isn’t found from the database.
  3. Add an inner class called WhenRequestedTodoItemIsFound to the FindById class. This inner class contains the test methods which ensure that the system under test is working as expected when the requested todo item is found from the database.

After we have created the required class hierarchy, the source code of 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.springframework.test.web.servlet.MockMvc;
 5import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 6
 7import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
 8import static org.mockito.Mockito.mock;
 9
10class TodoItemCrudControllerTest {
11
12    private TodoItemRequestBuilder requestBuilder;
13    private TodoItemCrudService service;
14
15    @BeforeEach
16    void configureSystemUnderTest() {
17        service = mock(TodoItemCrudService.class);
18
19        TodoItemCrudController testedController = new TodoItemCrudController(service);
20        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController)
21                .setControllerAdvice(new TodoItemErrorHandler())
22                .setMessageConverters(objectMapperHttpMessageConverter())
23                .build();
24        requestBuilder = new TodoItemRequestBuilder(mockMvc);
25    }
26
27    @Nested
28    @DisplayName("Find todo item by using its id as search criteria")
29    class FindById {
30
31        @Nested
32        @DisplayName("When the requested todo item isn't found")
33        class WhenRequestedTodoItemIsNotFound {
34
35        }
36
37        @Nested
38        @DisplayName("When the requested todo item is found")
39        class WhenRequestedTodoItemIsFound {
40
41        }
42    }
43}

Second, we have to ensure that the system under test is working as expected when the requested todo item isn’t found from the database. We can write the required test methods by following these steps:

  1. Add a constant called TODO_ITEM_ID to the FindById class. This constant specifies the id of the requested todo item. We have to add this constant to the FindById class because its value is used by the test methods found from the WhenRequestedTodoItemIsNotFound and WhenRequestedTodoItemIsFound classes.
  2. Add a new setup method to the WhenRequestedTodoItemIsNotFound class and ensure that it’s run before a test method is run. When we implement this setup method, we must ensure that the TodoItemCrudService object throws a TodoItemNotFoundException when its findById() method is invoked by using the argument: 1L.
  3. Ensure that the system under test returns the HTTP status code 404.
  4. Verify that the system under test returns an HTTP response which has an empty response body.

After we have written the required test methods, the source code of 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.Test;
 5import org.springframework.test.web.servlet.MockMvc;
 6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 7
 8import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
 9import static org.mockito.BDDMockito.given;
10import static org.mockito.Mockito.mock;
11import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
12import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
13
14class TodoItemCrudControllerTest {
15
16    private TodoItemRequestBuilder requestBuilder;
17    private TodoItemCrudService service;
18
19    @BeforeEach
20    void configureSystemUnderTest() {
21        service = mock(TodoItemCrudService.class);
22
23        TodoItemCrudController testedController = new TodoItemCrudController(service);
24        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController)
25                .setControllerAdvice(new TodoItemErrorHandler())
26                .setMessageConverters(objectMapperHttpMessageConverter())
27                .build();
28        requestBuilder = new TodoItemRequestBuilder(mockMvc);
29    }
30
31    @Nested
32    @DisplayName("Find todo item by using its id as search criteria")
33    class FindById {
34
35        private static final Long TODO_ITEM_ID = 1L;
36
37        @Nested
38        @DisplayName("When the requested todo item isn't found")
39        class WhenRequestedTodoItemIsNotFound {
40
41            @BeforeEach
42            void throwException() {
43                given(service.findById(TODO_ITEM_ID))
44                        .willThrow(new TodoItemNotFoundException(""));
45            }
46
47            @Test
48            @DisplayName("Should return the HTTP status code not found (404)")
49            void shouldReturnHttpStatusCodeNotFound() throws Exception {
50                requestBuilder.findById(TODO_ITEM_ID)
51                        .andExpect(status().isNotFound());
52            }
53
54            @Test
55            @DisplayName("Should return HTTP response which has an empty response body")
56            void shouldReturnHttpResponseWhichHasEmptyResponseBody() throws Exception {
57                requestBuilder.findById(TODO_ITEM_ID)
58                        .andExpect(content().string(""));
59            }
60        }
61
62        //The other inner class is omitted
63    }
64}

Third, we have to ensure that the system under test is working as expected when the requested todo item is found from the database. We can write the required test methods by following these steps:

  1. Add the required constants to the WhenRequestedTodoItemIsFound class. These constants specify the property values of the found todo item.
  2. Add a new setup method to the WhenRequestedTodoItemIsFound class and ensure that it’s run before a test method is run. When we implement this setup method, we must ensure that the TodoItemCrudService object returns the information of the found todo item when its findById() method is invoked by using the argument: 1L.
  3. Ensure that the system under test returns the HTTP status code 200.
  4. Verify that the system under test returns the information of the found todo item as JSON.
  5. Ensure that the system under test returns the information of the found todo item.
  6. Verify that the system under test returns the information of a todo item that has one tag.
  7. Ensure that the system under test returns the information of the found tag.

After we have written the required test methods, the source code of 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.Test;
  5import org.springframework.http.MediaType;
  6import org.springframework.test.web.servlet.MockMvc;
  7import org.springframework.test.web.servlet.setup.MockMvcBuilders;
  8
  9import java.util.Arrays;
 10
 11import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
 12import static org.hamcrest.Matchers.equalTo;
 13import static org.hamcrest.Matchers.hasSize;
 14import static org.mockito.BDDMockito.given;
 15import static org.mockito.Mockito.mock;
 16import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
 17import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
 18import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 19
 20class TodoItemCrudControllerTest {
 21
 22    private TodoItemRequestBuilder requestBuilder;
 23    private TodoItemCrudService service;
 24
 25    @BeforeEach
 26    void configureSystemUnderTest() {
 27        service = mock(TodoItemCrudService.class);
 28
 29        TodoItemCrudController testedController = new TodoItemCrudController(service);
 30        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController)
 31                .setControllerAdvice(new TodoItemErrorHandler())
 32                .setMessageConverters(objectMapperHttpMessageConverter())
 33                .build();
 34        requestBuilder = new TodoItemRequestBuilder(mockMvc);
 35    }
 36
 37    @Nested
 38    @DisplayName("Find todo item by using its id as search criteria")
 39    class FindById {
 40
 41        private static final Long TODO_ITEM_ID = 1L;
 42
 43        //The other inner class is omitted
 44
 45        @Nested
 46        @DisplayName("When the requested todo item is found")
 47        class WhenRequestedTodoItemIsFound {
 48
 49            private static final String DESCRIPTION = "Remember to use JUnit 5";
 50            private static final Long TAG_ID = 9L;
 51            private static final String TAG_NAME  = "Code";
 52            private static final String TITLE = "Write example application";
 53            private static final TodoItemStatus STATUS = TodoItemStatus.IN_PROGRESS;
 54
 55            @BeforeEach
 56            void returnFoundTodoItem() {
 57                TodoItemDTO found = new TodoItemDTO();
 58                found.setId(TODO_ITEM_ID);
 59                found.setDescription(DESCRIPTION);
 60                found.setStatus(STATUS);
 61                found.setTitle(TITLE);
 62
 63                TagDTO tag = new TagDTO();
 64                tag.setId(TAG_ID);
 65                tag.setName(TAG_NAME);
 66                found.setTags(Arrays.asList(tag));
 67
 68                given(service.findById(TODO_ITEM_ID)).willReturn(found);
 69            }
 70
 71            @Test
 72            @DisplayName("Should return the HTTP status code ok (200)")
 73            void shouldReturnHttpStatusCodeOk() throws Exception {
 74                requestBuilder.findById(TODO_ITEM_ID)
 75                        .andExpect(status().isOk());
 76            }
 77
 78            @Test
 79            @DisplayName("Should return the information of the found todo item as JSON")
 80            void shouldReturnInformationOfFoundTodoItemAsJSON() throws Exception {
 81                requestBuilder.findById(TODO_ITEM_ID)
 82                        .andExpect(content().contentType(MediaType.APPLICATION_JSON));
 83            }
 84
 85            @Test
 86            @DisplayName("Should return the information of the found todo item")
 87            void shouldReturnInformationOfFoundTodoItem() throws Exception {
 88                requestBuilder.findById(TODO_ITEM_ID)
 89                        .andExpect(jsonPath("$.id",
 90                                equalTo(TODO_ITEM_ID.intValue()))
 91                        )
 92                        .andExpect(jsonPath("$.description",
 93                                equalTo(DESCRIPTION))
 94                        )
 95                        .andExpect(jsonPath("$.status",
 96                                equalTo(STATUS.name()))
 97                        )
 98                        .andExpect(jsonPath("$.title",
 99                                equalTo(TITLE))
100                        );
101            }
102
103            @Test
104            @DisplayName("Should return a todo item that has one tag")
105            void shouldReturnTodoItemThatHasOneTag() throws Exception {
106                requestBuilder.findById(TODO_ITEM_ID)
107                        .andExpect(jsonPath("$.tags", hasSize(1)));
108            }
109
110            @Test
111            @DisplayName("Should return the information of the found tag")
112            void shouldReturnInformationOfFoundTag() throws Exception {
113                requestBuilder.findById(TODO_ITEM_ID)
114                        .andExpect(jsonPath("$.tags[0].id",
115                                equalTo(TAG_ID.intValue()))
116                        )
117                        .andExpect(jsonPath("$.tags[0].name",
118                                equalTo(TAG_NAME))
119                        );
120            }
121        }
122    }
123}

We can now write unit tests for a controller method which returns the information of a single item as JSON. Let’s summarize what we learned from this blog post.

Summary

This blog post has taught us four things:

  • When we want to write assertions for the returned HTTP status, we have to invoke the status() method of the MockMvcResultMatchers class.
  • When we want to write assertions for the content of the returned HTTP response, we have to invoke the content() method of the MockMvcResultMatchers class.
  • When we want to write assertions for the body of the returned HTTP response by using JsonPath expressions and Hamcrest matchers, we have to invoke the jsonPath() method of the MockMvcResultMatchers class.
  • If we want to write assertions for the body of the returned HTTP response by using JsonPath expressions and Hamcrest matchers, we must ensure that the json-path and hamcrest-library dependencies are found from the classpath

P.S. You can get the example application of this blog post from Github.