The previous part of my Spring MVC Test tutorial described how we can write unit tests for Spring MVC controllers which render the information of a single item. This blog post provides more information about writing unit tests for Spring MVC controllers which render a view. To be more specific, this blog post describes how we can write unit tests for a Spring MVC controller that renders a list.

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 renders the correct view.
  • Understand how we can ensure that our model attributes contain the correct information.

Let’s begin.

Introduction to the System Under Test

We have to write unit tests for a controller method which processes GET requests send to the path: ‘/todo-item’. This method returns the HTTP status code 200 and renders the information of all todo items which are found from the database. If no todo items are found from the database, this controller method returns the HTTP status code 200 and renders an empty list.

The tested controller method is called findAll() and it’s implemented by following these steps:

  1. Find the todo items from the database by invoking the findAll() method of the TodoItemCrudService class.
  2. Put the found todo items to a model attribute called todoItems.
  3. Return the name of the view (’todo-item/list’) that renders the information of the found todo items.

The source code of the tested controller method looks as follows:

 1import org.springframework.beans.factory.annotation.Autowired;
 2import org.springframework.stereotype.Controller;
 3import org.springframework.ui.Model;
 4import org.springframework.web.bind.annotation.GetMapping;
 5import org.springframework.web.bind.annotation.RequestMapping;
 6
 7import java.util.List;
 8
 9@Controller
10@RequestMapping("/todo-item")
11public class TodoItemCrudController {
12
13    private final TodoItemCrudService service;
14
15    @Autowired
16    public TodoItemCrudController(TodoItemCrudService service) {
17        this.service = service;
18    }
19
20    @GetMapping
21    public String findAll(Model model) {
22        List<TodoListItemDTO> todoItems = service.findAll();
23        model.addAttribute("todoItems", todoItems);
24        return "todo-item/list";
25    }
26}

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}

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 renders data, we have to learn how we can write assertions for the response returned by the system under test. When we want to write assertions for the 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 view() method returns a ViewResultMatchers object which allows us to write assertions for the rendered view.
  • The model() method returns a ModelResultMatchers object which allows us to write assertions for the Spring MVC model.

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 and 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 WhenNoTodoItemsAreFoundFromDatabase 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 WhenTwoTodoItemsAreFoundFromDatabase 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        MockMvc mockMvc = MockMvcBuilders
20                .standaloneSetup(new TodoItemCrudController(service))
21                .setHandlerExceptionResolvers(exceptionResolver())
22                .setLocaleResolver(fixedLocaleResolver())
23                .setViewResolvers(jspViewResolver())
24                .build();
25        requestBuilder = new TodoItemRequestBuilder(mockMvc);
26    }
27
28    @Nested
29    @DisplayName("Render the information of all todo items")
30    class FindAll {
31
32        @Nested
33        @DisplayName("When no todo items are found from the database")
34        class WhenNoTodoItemsAreFoundFromDatabase {
35
36        }
37
38        @Nested
39        @DisplayName("When two todo items are found from the database")
40        class WhenTwoTodoItemsAreFoundFromDatabase {
41
42        }
43    }
44}

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 renders the list view.

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.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.Mockito.mock;
10import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
11import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
12
13class TodoItemCrudControllerTest {
14
15    private TodoItemRequestBuilder requestBuilder;
16    private TodoItemCrudService service;
17
18    @BeforeEach
19    void configureSystemUnderTest() {
20        service = mock(TodoItemCrudService.class);
21
22        MockMvc mockMvc = MockMvcBuilders
23                .standaloneSetup(new TodoItemCrudController(service))
24                .setHandlerExceptionResolvers(exceptionResolver())
25                .setLocaleResolver(fixedLocaleResolver())
26                .setViewResolvers(jspViewResolver())
27                .build();
28        requestBuilder = new TodoItemRequestBuilder(mockMvc);
29    }
30
31    @Nested
32    @DisplayName("Render the information of all todo items")
33    class FindAll {
34
35        @Test
36        @DisplayName("Should return the HTTP status code 200")
37        void shouldReturnHttpStatusCodeOk() throws Exception {
38            requestBuilder.findAll().andExpect(status().isOk());
39        }
40
41        @Test
42        @DisplayName("Should render the todo item list view")
43        void shouldRenderTodoItemListView() throws Exception {
44            requestBuilder.findAll().andExpect(view().name("todo-item/list"));
45        }
46
47        //The inner classes are omitted
48    }
49}

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 WhenNoTodoItemsAreFoundFromDatabase 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 displays zero todo items.

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.test.web.servlet.MockMvc;
 6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 7
 8import java.util.ArrayList;
 9
10import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
11import static org.hamcrest.Matchers.hasSize;
12import static org.mockito.BDDMockito.given;
13import static org.mockito.Mockito.mock;
14import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
15import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
16import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
17
18class TodoItemCrudControllerTest {
19
20    private TodoItemRequestBuilder requestBuilder;
21    private TodoItemCrudService service;
22
23    @BeforeEach
24    void configureSystemUnderTest() {
25        service = mock(TodoItemCrudService.class);
26
27        MockMvc mockMvc = MockMvcBuilders
28                .standaloneSetup(new TodoItemCrudController(service))
29                .setHandlerExceptionResolvers(exceptionResolver())
30                .setLocaleResolver(fixedLocaleResolver())
31                .setViewResolvers(jspViewResolver())
32                .build();
33        requestBuilder = new TodoItemRequestBuilder(mockMvc);
34    }
35
36    @Nested
37    @DisplayName("Render the information of all todo items")
38    class FindAll {
39
40        @Test
41        @DisplayName("Should return the HTTP status code 200")
42        void shouldReturnHttpStatusCodeOk() throws Exception {
43            requestBuilder.findAll().andExpect(status().isOk());
44        }
45
46        @Test
47        @DisplayName("Should render the todo item list view")
48        void shouldRenderTodoItemListView() throws Exception {
49            requestBuilder.findAll().andExpect(view().name("todo-item/list"));
50        }
51
52        @Nested
53        @DisplayName("When no todo items are found from the database")
54        class WhenNoTodoItemsAreFoundFromDatabase {
55
56            @BeforeEach
57            void serviceReturnsEmptyList() {
58                given(service.findAll()).willReturn(new ArrayList<>());
59            }
60
61            @Test
62            @DisplayName("Should display zero todo items")
63            void shouldDisplayZeroTodoItems() throws Exception {
64                requestBuilder.findAll().andExpect(model().attribute(
65                        "todoItems",
66                        hasSize(0)
67                ));
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 WhenTwoTodoItemsAreFoundFromDatabase class. These constants specify the information of the found todo items.
  2. Add a new setup method to the WhenTwoTodoItemsAreFoundFromDatabase 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 displays two todo items.
  4. Verify that the system under test displays the correct information of the first todo item.
  5. Ensure that the system under test displays the correct information of the second todo item.
  6. Verify that the system under test displays the todo items in the correct order.

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.test.web.servlet.MockMvc;
  6import org.springframework.test.web.servlet.setup.MockMvcBuilders;
  7
  8import java.util.Arrays;
  9
 10import static net.petrikainulainen.springmvctest.junit5.web.WebTestConfig.*;
 11import static org.hamcrest.Matchers.allOf;
 12import static org.hamcrest.Matchers.contains;
 13import static org.hamcrest.Matchers.equalTo;
 14import static org.hamcrest.Matchers.hasItem;
 15import static org.hamcrest.Matchers.hasProperty;
 16import static org.hamcrest.Matchers.hasSize;
 17import static org.mockito.BDDMockito.given;
 18import static org.mockito.Mockito.mock;
 19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
 20import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
 21import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
 22
 23class TodoItemCrudControllerTest {
 24
 25    private TodoItemRequestBuilder requestBuilder;
 26    private TodoItemCrudService service;
 27
 28    @BeforeEach
 29    void configureSystemUnderTest() {
 30        service = mock(TodoItemCrudService.class);
 31
 32        MockMvc mockMvc = MockMvcBuilders
 33                .standaloneSetup(new TodoItemCrudController(service))
 34                .setHandlerExceptionResolvers(exceptionResolver())
 35                .setLocaleResolver(fixedLocaleResolver())
 36                .setViewResolvers(jspViewResolver())
 37                .build();
 38        requestBuilder = new TodoItemRequestBuilder(mockMvc);
 39    }
 40
 41    @Nested
 42    @DisplayName("Render the information of all todo items")
 43    class FindAll {
 44
 45        @Test
 46        @DisplayName("Should return the HTTP status code 200")
 47        void shouldReturnHttpStatusCodeOk() throws Exception {
 48            requestBuilder.findAll().andExpect(status().isOk());
 49        }
 50
 51        @Test
 52        @DisplayName("Should render the todo item list view")
 53        void shouldRenderTodoItemListView() throws Exception {
 54            requestBuilder.findAll().andExpect(view().name("todo-item/list"));
 55        }
 56
 57        //The other inner class is omitted
 58
 59        @Nested
 60        @DisplayName("When two todo items are found from the database")
 61        class WhenTwoTodoItemsAreFoundFromDatabase {
 62
 63            private final Long TODO_ITEM_ONE_ID = 1L;
 64            private final String TODO_ITEM_ONE_TITLE = "first todo item";
 65            private final Long TODO_ITEM_TWO_ID = 2L;
 66            private final String TODO_ITEM_TWO_TITLE = "second todo item";
 67
 68            private final TodoItemStatus STATUS_OPEN = TodoItemStatus.OPEN;
 69
 70            @BeforeEach
 71            void serviceReturnsTwoTodoItems() {
 72                TodoListItemDTO first = new TodoListItemDTO();
 73                first.setId(TODO_ITEM_ONE_ID);
 74                first.setTitle(TODO_ITEM_ONE_TITLE);
 75                first.setStatus(STATUS_OPEN);
 76
 77                TodoListItemDTO second = new TodoListItemDTO();
 78                second.setId(TODO_ITEM_TWO_ID);
 79                second.setTitle(TODO_ITEM_TWO_TITLE);
 80                second.setStatus(STATUS_OPEN);
 81
 82                given(service.findAll()).willReturn(Arrays.asList(first, second));
 83            }
 84
 85            @Test
 86            @DisplayName("Should display two todo items")
 87            void shouldDisplayTwoTodoItems() throws Exception {
 88                requestBuilder.findAll().andExpect(model().attribute(
 89                        "todoItems",
 90                        hasSize(2)
 91                ));
 92            }
 93
 94            @Test
 95            @DisplayName("Should display the information of the first todo item")
 96            void shouldDisplayInformationOfFirstTodoItem() throws Exception {
 97                requestBuilder.findAll()
 98                        .andExpect(
 99                                model().attribute(
100                                        "todoItems",
101                                        hasItem(allOf(
102                                                hasProperty("id", equalTo(TODO_ITEM_ONE_ID)),
103                                                hasProperty("title", equalTo(TODO_ITEM_ONE_TITLE)),
104                                                hasProperty("status", equalTo(STATUS_OPEN))
105                                        ))
106                                )
107                        );
108            }
109
110            @Test
111            @DisplayName("Should display the information of the second todo item")
112            void shouldDisplayInformationOfSecondTodoItem() throws Exception {
113                requestBuilder.findAll()
114                        .andExpect(
115                                model().attribute(
116                                        "todoItems",
117                                        hasItem(allOf(
118                                                hasProperty("id", equalTo(TODO_ITEM_TWO_ID)),
119                                                hasProperty("title", equalTo(TODO_ITEM_TWO_TITLE)),
120                                                hasProperty("status", equalTo(STATUS_OPEN))
121                                        ))
122                                )
123                        );
124            }
125
126            @Test
127            @DisplayName("Should display the todo items in the correct order")
128            void shouldDisplayFirstAndSecondTodoItemInCorrectOrder() throws Exception {
129                requestBuilder.findAll()
130                        .andExpect(
131                                model().attribute(
132                                        "todoItems",
133                                        contains(
134                                                allOf(
135                                                        hasProperty("id", equalTo(TODO_ITEM_ONE_ID)),
136                                                        hasProperty("title", equalTo(TODO_ITEM_ONE_TITLE)),
137                                                        hasProperty("status", equalTo(STATUS_OPEN))
138                                                ),
139                                                allOf(
140                                                        hasProperty("id", equalTo(TODO_ITEM_TWO_ID)),
141                                                        hasProperty("title", equalTo(TODO_ITEM_TWO_TITLE)),
142                                                        hasProperty("status", equalTo(STATUS_OPEN))
143                                                )
144                                        )
145                                )
146                        );
147            }
148        }
149    }
150}

We can now write unit tests for a controller method that renders a list. 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 rendered view, we have to invoke the view() method of the MockMvcResultMatchers class.
  • When we want to write assertions for the Spring MVC model, we have to invoke the model() method of the MockMvcResultMatchers class.
  • We can use Hamcrest matchers for writing assertions for the model attributes found from the Spring MVC model.

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