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.
This blog post assumes that:
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:
- Find the todo items from the database by invoking the
findAll()method of theTodoItemCrudServiceclass. - Put the found todo items to a model attribute called
todoItems. - 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 aStatusResultMatchersobject which allows us to write assertions for the returned HTTP status. - The
view()method returns aViewResultMatchersobject which allows us to write assertions for the rendered view. - The
model()method returns aModelResultMatchersobject which allows us to write assertions for the Spring MVC model.
Additional Reading:
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:
- Add a new method called
findAll()to our request builder class and ensure that this method returns a ResultActions object. - Send a
GETrequest to the path: ‘/todo-item’ by invoking theperform()method of theMockMvcclass. Remember to return theResultActionsobject that’s returned by theperform()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}
Additional Reading:
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:
- Add an inner class called
FindAllto our test class. This inner class contains the test methods which ensure that the system under test is working as expected. - Add an inner class called
WhenNoTodoItemsAreFoundFromDatabaseto theFindAllclass. 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. - Add an inner class called
WhenTwoTodoItemsAreFoundFromDatabaseto theFindAllclass. 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:
- Ensure that the system under test returns the HTTP status code 200.
- 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:
- Add a new setup method to the
WhenNoTodoItemsAreFoundFromDatabaseclass and ensure that it’s run before a test method is run. When we implement this method, we must ensure that theTodoItemCrudServiceobject returns an empty list when itsfindAll()method is invoked. - 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:
- Add the required constants to the
WhenTwoTodoItemsAreFoundFromDatabaseclass. These constants specify the information of the found todo items. - Add a new setup method to the
WhenTwoTodoItemsAreFoundFromDatabaseclass and ensure that it’s run before a test method is run. When we implement this method, we must ensure that theTodoItemCrudServiceobject returns a list that contains two todo items when itsfindAll()method is invoked. - Ensure that the system under test displays two todo items.
- Verify that the system under test displays the correct information of the first todo item.
- Ensure that the system under test displays the correct information of the second todo item.
- 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}
Additional Reading:
- The Javadoc of the
MockMvcResultMatchersclass - The Javadoc of the
ModelResultMatchersclass - [The Javadoc of the
StatusResultMatchersclass - The Javadoc of the
ViewResultMatchersclass](https://docs.spring.io/spring-framework/docs/5.3.x/javadoc-api/org/springframework/test/web/servlet/result/StatusResultMatchers.html) - The Javadoc of the
Matchersclass
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 theMockMvcResultMatchersclass. - When we want to write assertions for the rendered view, we have to invoke the
view()method of theMockMvcResultMatchersclass. - When we want to write assertions for the Spring MVC model, we have to invoke the
model()method of theMockMvcResultMatchersclass. - 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.