The previous part of my Spring MVC Test tutorial described how we can write unit tests for Spring MVC controllers which return the information of a single item as JSON. This blog post provides more information about writing unit tests for a Spring MVC REST API. To be more specific, this blog post describes how we can write unit tests for a Spring MVC controller that returns a list 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’. The contract of this API endpoint is described in the following:

  • The system under test always returns the HTTP status code 200.
  • If todo items are found, the system under test creates a JSON document which contains a list of found todo items and adds this document to the body of the returned HTTP response.
  • If no todo items are found, the system under test creates a JSON document which contains an empty list and adds this document to the body of the returned HTTP response.

The tested controller method is called findAll() and it simply returns the todo items which are 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.RequestMapping;
 4import org.springframework.web.bind.annotation.RestController;
 5
 6import java.util.List;
 7
 8@RestController
 9@RequestMapping("/todo-item")
10public class TodoItemCrudController {
11    private final TodoItemCrudService service;
12
13    @Autowired
14    public TodoItemCrudController(TodoItemCrudService service) {
15        this.service = service;
16    }
17
18    @GetMapping
19    public List<TodoListItemDTO> findAll() {
20        return service.findAll();
21    }
22}

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

1public class TodoListItemDTO {
2
3    private Long id;
4    private String title;
5    private TodoItemStatus status;
6
7    //Getters and setters are omitted
8}

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 two todo items are found from the database, the system under test returns the following JSON document back to the client:

 1[
 2	{
 3		"id":1,
 4		"title":"Write example application",
 5		"status":"DONE"
 6	},
 7	{
 8		"id":2,
 9		"title":"Write blog post",
10		"status":"IN_PROGRESS"
11	}
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 a list 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 findAll() to our request builder class. Ensure that this method returns a ResultActions object.
  2. Send a GET request to the path: ‘/todo-item’ 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 findAll() throws Exception {
15        return mockMvc.perform(get("/todo-item"));
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 FindAll 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 WhenNoTodoItemsAreFound to the FindAll class. This inner class contains the test methods which ensure that the system under test is working as expected when no todo items are found from the database.
  3. Add an inner class called WhenTwoTodoItemsAreFound to the FindAll class. This inner class contains the test methods which ensure that the system under test is working as expected when two todo items are 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 all todo items")
29    class FindAll {
30
31        @Nested
32        @DisplayName("When no todo items are found")
33        class WhenNoTodoItemsAreFound {
34
35        }
36
37        @Nested
38        @DisplayName("When two todo items are found")
39        class WhenTwoTodoItemsAreFound {
40
41        }
42    }
43}

Second, because we don’t want to add duplicate code to our test class, we will add some test methods to the FindAll class. These unit tests specify the behavior of the system under test in all possible scenarios. We can write these unit tests by following these steps:

  1. Ensure that the system under test returns the HTTP status code 200.
  2. Verify that the system under test returns the information of the found todo items as JSON.

After we have written these unit tests, 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 static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
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 all todo items")
33    class FindAll {
34
35        @Test
36        @DisplayName("Should return the HTTP status code OK (200)")
37        void shouldReturnHttpStatusCodeOk() throws Exception {
38            requestBuilder.findAll()
39                    .andExpect(status().isOk());
40        }
41
42        @Test
43        @DisplayName("Should return the found todo items as JSON")
44        void shouldReturnFoundTodoItemAsJSON() throws Exception {
45            requestBuilder.findAll()
46                    .andExpect(content().contentType(MediaType.APPLICATION_JSON));
47        }
48
49        //The other inner classes are omitted
50    }
51}

Third, we have to write the unit tests which ensure that the system under test is working as expected when no todo items are found from the database. We can write the required test methods by following these steps:

  1. Add a new setup method to the WhenNoTodoItemsAreFound class and ensure that it’s run before a test method is run. When we implement this method, we must ensure that the TodoItemCrudService object returns an empty list when its findAll() method is invoked.
  2. Ensure that the system under test returns a JSON document that contains an empty list.

After we have written the required unit tests, 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.ArrayList;
10
11import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
12import static org.hamcrest.Matchers.hasSize;
13import static org.mockito.BDDMockito.given;
14import static org.mockito.Mockito.mock;
15import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
16import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
17import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
18
19class TodoItemCrudControllerTest {
20
21    private TodoItemRequestBuilder requestBuilder;
22    private TodoItemCrudService service;
23
24    @BeforeEach
25    void configureSystemUnderTest() {
26        service = mock(TodoItemCrudService.class);
27
28        TodoItemCrudController testedController = new TodoItemCrudController(service);
29        MockMvc mockMvc = MockMvcBuilders.standaloneSetup(testedController)
30                .setControllerAdvice(new TodoItemErrorHandler())
31                .setMessageConverters(objectMapperHttpMessageConverter())
32                .build();
33        requestBuilder = new TodoItemRequestBuilder(mockMvc);
34    }
35
36    @Nested
37    @DisplayName("Find all todo items")
38    class FindAll {
39
40        @Test
41        @DisplayName("Should return the HTTP status code OK (200)")
42        void shouldReturnHttpStatusCodeOk() throws Exception {
43            requestBuilder.findAll()
44                    .andExpect(status().isOk());
45        }
46
47        @Test
48        @DisplayName("Should return the found todo items as JSON")
49        void shouldReturnFoundTodoItemAsJSON() throws Exception {
50            requestBuilder.findAll()
51                    .andExpect(content().contentType(MediaType.APPLICATION_JSON));
52        }
53
54        @Nested
55        @DisplayName("When no todo items are found")
56        class WhenNoTodoItemsAreFound {
57
58            @BeforeEach
59            void returnEmptyList() {
60                given(service.findAll()).willReturn(new ArrayList<>());
61            }
62
63            @Test
64            @DisplayName("Should return zero todo items")
65            void shouldReturnZeroTodoItems() throws Exception {
66                requestBuilder.findAll()
67                        .andExpect(jsonPath("$", hasSize(0)));
68            }
69        }
70
71        //The other inner class is omitted
72    }
73}

Fourth, we have to write the unit tests which ensure that the system under test is working as expected when two todo items are found from the database. We can write the required test methods by following these steps:

  1. Add the required constants to the WhenTwoTodoItemsAreFound class. These constants specify the information of the found todo items.
  2. Add a new setup method to the WhenTwoTodoItemsAreFound class and ensure that it’s run before a test method is run. When we implement this method, we must ensure that the TodoItemCrudService object returns a list that contains two todo items when its findAll() method is invoked.
  3. Ensure that the system under test returns a JSON document that contains two todo items.
  4. Verify that the system under test returns the correct information of the first todo item.
  5. Ensure that the system under test returns the correct information of the second todo item.

After we have written the required unit tests, 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 all todo items")
 39    class FindAll {
 40
 41        @Test
 42        @DisplayName("Should return the HTTP status code OK (200)")
 43        void shouldReturnHttpStatusCodeOk() throws Exception {
 44            requestBuilder.findAll()
 45                    .andExpect(status().isOk());
 46        }
 47
 48        @Test
 49        @DisplayName("Should return the found todo items as JSON")
 50        void shouldReturnFoundTodoItemAsJSON() throws Exception {
 51            requestBuilder.findAll()
 52                    .andExpect(content().contentType(MediaType.APPLICATION_JSON));
 53        }
 54
 55        //The other inner class is omitted
 56
 57        @Nested
 58        @DisplayName("When two todo items are found")
 59        class WhenTwoTodoItemsAreFound {
 60
 61            private static final Long FIRST_TODO_ITEM_ID = 1L;
 62            private static final TodoItemStatus FIRST_TODO_ITEM_STATUS = TodoItemStatus.DONE;
 63            private static final String FIRST_TODO_ITEM_TITLE = "Write example application";
 64
 65            private static final Long SECOND_TODO_ITEM_ID = 2L;
 66            private static final TodoItemStatus SECOND_TODO_ITEM_STATUS = TodoItemStatus.IN_PROGRESS;
 67            private static final String SECOND_TODO_ITEM_TITLE = "Write blog post";
 68
 69            @BeforeEach
 70            void returnTwoTodoItems() {
 71                TodoListItemDTO first = new TodoListItemDTO();
 72                first.setId(FIRST_TODO_ITEM_ID);
 73                first.setStatus(FIRST_TODO_ITEM_STATUS);
 74                first.setTitle(FIRST_TODO_ITEM_TITLE);
 75
 76                TodoListItemDTO second = new TodoListItemDTO();
 77                second.setId(SECOND_TODO_ITEM_ID);
 78                second.setStatus(SECOND_TODO_ITEM_STATUS);
 79                second.setTitle(SECOND_TODO_ITEM_TITLE);
 80
 81                given(service.findAll()).willReturn(Arrays.asList(first, second));
 82            }
 83
 84            @Test
 85            @DisplayName("Should return two todo items")
 86            void shouldReturnTwoTodoItems() throws Exception {
 87                requestBuilder.findAll()
 88                        .andExpect(jsonPath("$", hasSize(2)));
 89            }
 90
 91            @Test
 92            @DisplayName("Should return the information of the first todo item")
 93            void shouldReturnInformationOfFirstTodoItem() throws Exception {
 94                requestBuilder.findAll()
 95                        .andExpect(jsonPath("$[0].id",
 96                                equalTo(FIRST_TODO_ITEM_ID.intValue()))
 97                        )
 98                        .andExpect(jsonPath("$[0].status",
 99                                equalTo(FIRST_TODO_ITEM_STATUS.name()))
100                        )
101                        .andExpect(jsonPath("$[0].title",
102                                equalTo(FIRST_TODO_ITEM_TITLE))
103                        );
104            }
105
106            @Test
107            @DisplayName("Should return the information of the second todo item")
108            void shouldReturnInformationOfSecondTodoItem() throws Exception {
109                requestBuilder.findAll()
110                        .andExpect(jsonPath("$[1].id",
111                                equalTo(SECOND_TODO_ITEM_ID.intValue()))
112                        )
113                        .andExpect(jsonPath("$[1].status",
114                                equalTo(SECOND_TODO_ITEM_STATUS.name()))
115                        )
116                        .andExpect(jsonPath("$[1].title",
117                                equalTo(SECOND_TODO_ITEM_TITLE))
118                        );
119            }
120        }
121    }
122}

We can now write unit tests for a controller method which returns a list 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.