Writing Unit Tests for "Normal" Controllers - Rendering Data
After we have finished this lesson, we
- Know how we can create HTTP requests that are processed by the tested controller methods.
- Can ensure that the correct HTTP status code is returned.
- Can verify that the correct view is rendered.
- Can ensure that our model attributes contain the correct information.
This lesson assumes that:
Learning the Basics
Before we can write unit tests for a Spring MVC controller, we have to know how we can send an HTTP request to the tested controller and we must be able to write assertions for the returned response.
First, we can create HTTP requests by using the factory methods provided by the MockMvcRequestBuilders class. This class provides factory methods that we can use for creating DELETE, GET, POST, PUT, and PATCH requests.
These methods return either the MockHttpServletRequestBuilder or MockMultipartHttpServletRequestBuilder objects. We can use these objects for configuring the input that is send to the tested controller method. For example, we can set the request body, add cookies or headers to the request, and configure the request parameters by using the methods provided by these classes.
After we have created the HTTP request, we send can it to the tested controller method by invoking the perform() method of the MockMvc class.
I recommend that you study the API documentation of these classes because it helps you see that you can do everything you need with the Spring MVC Test framework.
Additional Reading:
Second, we can write assertions for the returned response by invoking the andExpect() method of the ResultActions interface. This method takes a ResultMather object as a method parameter. We can create new ResultMatcher objects by using the static factory methods provided by the MockMvcResultMatchers class.
For example, we can write assertions for the returned HTTP status code, the name of the rendered view, the model attributes of the Spring MVC Model, and so on. Also, we can write complex assertions by using Hamcrest Matchers.
Again, I recommend that you study the API documentation of the MockMvcResultMatchers class because it helps you see that you can write every assertion you might need.
Additional Reading:
Let’s move on and find out how we can use these classes when we are writing unit tests for normal Spring MVC controllers that read information from the database.
Test 1: Rendering All Tasks Found From the Database
We have to write unit tests for a controller method that processes GET requests send to the path: ‘/’. This method is method called showTaskList() and it is implemented by following these steps:
- Find tasks from the database by using the
findAll()method of theTaskCrudServiceinterface. - Put the found tasks to a model attribute called
tasks. - Return the name of the view (’task/list’) that renders the task list.
The source code of the tested controller method looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.context.MessageSource;
3import org.springframework.stereotype.Controller;
4import org.springframework.ui.Model;
5import org.springframework.web.bind.annotation.RequestMapping;
6import org.springframework.web.bind.annotation.RequestMethod;
7
8import java.util.List;
9
10@Controller
11public class TaskCrudController {
12
13 private final TaskCrudService crudService;
14 private final MessageSource messageSource;
15
16 @Autowired
17 TaskCrudController(TaskCrudService crudService,
18 MessageSource messageSource) {
19 this.crudService = crudService;
20 this.messageSource = messageSource;
21 }
22
23 @RequestMapping(value = "/", method = RequestMethod.GET)
24 public String showTaskList(Model model) {
25 List<TaskListDTO> tasks = crudService.findAll();
26 model.addAttribute("tasks", tasks);
27 return "task/list";
28 }
29}
The TaskListDTO is a simple DTO class that contains the information which is shown on the task list page. Its source code looks as follows:
1public class TaskListDTO {
2
3 private Long id;
4 private String title;
5 private TaskStatus status;
6
7 //Getters and setters are omitted
8}
Next, we will write unit tests which ensure that the showTaskList() method is working as expected when two tasks are found. We can write these tests by following these steps:
First, We have to configure the system under test in the setup method that is annotated with the @Before annotation. Because we will use the same configuration that I described in the previous part of this topic
, I won’t repeat myself here. However, I want to point out that this time we don’t have to add any additional components into the system under test.
After we have configured the system under test, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.context.support.StaticMessageSource;
6import org.springframework.test.web.servlet.MockMvc;
7import org.springframework.test.web.servlet.setup.MockMvcBuilders;
8
9import static org.mockito.Mockito.mock;
10
11@RunWith(HierarchicalContextRunner.class)
12@Category(UnitTest.class)
13public class TaskCrudControllerTest {
14
15 private TaskCrudService crudService;
16 private StaticMessageSource messageSource;
17 private MockMvc mockMvc;
18
19 @Before
20 public void configureSystemUnderTest() {
21 crudService = mock(TaskCrudService.class);
22 messageSource = new StaticMessageSource();
23
24 TaskCrudController controller = new TaskCrudController(crudService,
25 messageSource
26 );
27 mockMvc = MockMvcBuilders.standaloneSetup(controller)
28 .build();
29 }
30}
TaskCrudService is actually a stub, but I use the term mock here because the other tests, which are described in the next lesson of this topic, will verify interactions that happened between the system under test and our test double.
Second, we have to create the class hierarchy that we use when we configure our test cases and write our unit tests. After we have created our class hierarchy, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.context.support.StaticMessageSource;
6import org.springframework.test.web.servlet.MockMvc;
7import org.springframework.test.web.servlet.setup.MockMvcBuilders;
8
9import static org.mockito.Mockito.mock;
10
11@RunWith(HierarchicalContextRunner.class)
12@Category(UnitTest.class)
13public class TaskCrudControllerTest {
14
15 private TaskCrudService crudService;
16 private StaticMessageSource messageSource;
17 private MockMvc mockMvc;
18
19 @Before
20 public void configureSystemUnderTest() {
21 crudService = mock(TaskCrudService.class);
22 messageSource = new StaticMessageSource();
23
24 TaskCrudController controller = new TaskCrudController(crudService,
25 messageSource
26 );
27 mockMvc = MockMvcBuilders.standaloneSetup(controller)
28 .build();
29 }
30
31 public class ShowTaskList {
32
33 public class WhenTwoTasksAreFound {
34
35 //Add tests here
36 }
37 }
38}
Third, we have to configure our test cases. We can do this by following these steps:
- Add two
TaskListDTOfields into theWhenTwoTasksAreFoundclass. - Add a factory method called
createTask()into theWhenTwoTasksAreFoundclass. This method creates a newTaskListDTOobject and returns the created object. - Add a setup method into
WhenTwoTasksAreFoundclass. - Implement the setup method by creating two
TaskListDTOobjects and ensuring that theTaskCrudServicemock returns the created tasks when itsfindAll()method is invoked.
After we have configured our test cases, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.experimental.categories.Category;
4import org.junit.runner.RunWith;
5import org.springframework.context.support.StaticMessageSource;
6import org.springframework.test.web.servlet.MockMvc;
7import org.springframework.test.web.servlet.setup.MockMvcBuilders;
8
9import java.util.Arrays;
10
11import static org.mockito.BDDMockito.given;
12import static org.mockito.Mockito.mock;
13
14@RunWith(HierarchicalContextRunner.class)
15@Category(UnitTest.class)
16public class TaskCrudControllerTest {
17
18 private TaskCrudService crudService;
19 private StaticMessageSource messageSource;
20 private MockMvc mockMvc;
21
22 @Before
23 public void configureSystemUnderTest() {
24 crudService = mock(TaskCrudService.class);
25 messageSource = new StaticMessageSource();
26
27 TaskCrudController controller = new TaskCrudController(crudService,
28 messageSource
29 );
30 mockMvc = MockMvcBuilders.standaloneSetup(controller)
31 .build();
32 }
33
34 public class ShowTaskList {
35
36 public class WhenTwoTasksAreFound {
37
38 private TaskListDTO first;
39 private TaskListDTO second;
40
41 @Before
42 public void returnTwoTasks() {
43 first = createTask(1L, "title", TaskStatus.OPEN);
44 second = createTask(33L, "secondTask", TaskStatus.OPEN);
45
46 given(crudService.findAll()).willReturn(Arrays.asList(first,
47 second
48 ));
49 }
50
51 private TaskListDTO createTask(Long id,
52 String title,
53 TaskStatus status) {
54 TaskListDTO task = new TaskListDTO();
55
56 task.setId(id);
57 task.setTitle(title);
58 task.setStatus(status);
59
60 return task;
61 }
62 }
63 }
64}
Fourth, we have to write five unit tests for the showTaskList() method of the TaskCrudController class:
- We have to verify that it returns the HTTP status code 200.
- We have to ensure that it renders the task list view.
- We have to verify that the rendered task list contains two tasks.
- We have to ensure that the tasks are rendered in the correct order.
- We have to verify that the rendered task list contains the correct information.
After we have written these unit tests, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.Test;
4import org.junit.experimental.categories.Category;
5import org.junit.runner.RunWith;
6import org.springframework.context.support.StaticMessageSource;
7import org.springframework.test.web.servlet.MockMvc;
8import org.springframework.test.web.servlet.setup.MockMvcBuilders;
9
10import java.util.Arrays;
11
12import static org.hamcrest.Matchers.allOf;
13import static org.hamcrest.Matchers.contains;
14import static org.hamcrest.Matchers.hasItem;
15import static org.hamcrest.Matchers.hasProperty;
16import static org.hamcrest.Matchers.hasSize;
17import static org.hamcrest.Matchers.is;
18import static org.mockito.BDDMockito.given;
19import static org.mockito.Mockito.mock;
20import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
21import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
22import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
24
25@RunWith(HierarchicalContextRunner.class)
26@Category(UnitTest.class)
27public class TaskCrudControllerTest {
28
29 private TaskCrudService crudService;
30 private StaticMessageSource messageSource;
31 private MockMvc mockMvc;
32
33 @Before
34 public void configureSystemUnderTest() {
35 crudService = mock(TaskCrudService.class);
36 messageSource = new StaticMessageSource();
37
38 TaskCrudController controller = new TaskCrudController(crudService,
39 messageSource
40 );
41 mockMvc = MockMvcBuilders.standaloneSetup(controller)
42 .build();
43 }
44
45 public class ShowTaskList {
46
47 public class WhenTwoTasksAreFound {
48
49 private TaskListDTO first;
50 private TaskListDTO second;
51
52 @Before
53 public void returnTwoTasks() {
54 first = createTask(1L, "title", TaskStatus.OPEN);
55 second = createTask(33L, "secondTask", TaskStatus.OPEN);
56
57 given(crudService.findAll()).willReturn(Arrays.asList(first,
58 second
59 ));
60 }
61
62 private TaskListDTO createTask(Long id,
63 String title,
64 TaskStatus status) {
65 TaskListDTO task = new TaskListDTO();
66
67 task.setId(id);
68 task.setTitle(title);
69 task.setStatus(status);
70
71 return task;
72 }
73
74 @Test
75 public void shouldReturnHttpStatusCodeOk() throws Exception {
76 mockMvc.perform(get("/"))
77 .andExpect(status().isOk());
78 }
79
80 @Test
81 public void shouldRenderTaskListView() throws Exception {
82 mockMvc.perform(get("/"))
83 .andExpect(view().name("task/list"));
84 }
85
86 @Test
87 public void shouldShowTaskListThatHasTwoTasks() throws Exception {
88 mockMvc.perform(get("/"))
89 .andExpect(model().attribute("tasks", hasSize(2)));
90 }
91
92 @Test
93 public void shouldShowTwoTasksInCorrectOrder() throws Exception {
94 mockMvc.perform(get("/"))
95 .andExpect(model().attribute("tasks", contains(first,
96 second
97 )));
98 }
99
100 @Test
101 public void shouldShowCorrectInformation() throws Exception {
102 mockMvc.perform(get("/"))
103 .andExpect(model().attribute("tasks", allOf(
104 hasItem(allOf(
105 hasProperty("id", is(1L)),
106 hasProperty("title", is("title")),
107 hasProperty("status", is(TaskStatus.OPEN))
108 )),
109 hasItem(allOf(
110 hasProperty("id", is(33L)),
111 hasProperty("title", is("secondTask")),
112 hasProperty("status", is(TaskStatus.OPEN))
113 ))
114 )));
115 }
116 }
117 }
118}
Additional Reading:
Let’s move on and write unit tests for a controller method that renders the information of a single task.
Test 2: Rendering the Information of a Single Task
We have to write unit tests for a controller method that processes GET requests send to the path: ‘/task/{taskId}’. This method is method called showTask() and it is implemented by following these steps:
- Find the task from the database by invoking the
findById()method of theTaskCrudServiceinterface. Pass the task id, which is given as a path variable, as a method parameter. - Put the found task to a model attribute called
task. - Return the name of the view (’task/view’) that renders the information of the found task.
The source code of the tested controller method looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.context.MessageSource;
3import org.springframework.stereotype.Controller;
4import org.springframework.ui.Model;
5import org.springframework.web.bind.annotation.PathVariable;
6import org.springframework.web.bind.annotation.RequestMapping;
7import org.springframework.web.bind.annotation.RequestMethod;
8
9import javax.validation.Valid;
10import java.util.List;
11import java.util.Locale;
12
13@Controller
14public class TaskCrudController {
15
16 private final TaskCrudService crudService;
17 private final MessageSource messageSource;
18
19 @Autowired
20 TaskCrudController(TaskCrudService crudService,
21 MessageSource messageSource) {
22 this.crudService = crudService;
23 this.messageSource = messageSource;
24 }
25
26 @RequestMapping(value = "/task/{taskId}", method = RequestMethod.GET)
27 public String showTask(@PathVariable("taskId") Long taskId,
28 Model model) {
29 TaskDTO task = crudService.findById(taskId);
30 model.addAttribute("task", task);
31 return "task/view";
32 }
33}
The TaskDTO class is a DTO that contains the information of a single task. Its source code looks as follows:
1public class TaskDTO {
2
3 private Long assigneeId;
4 private Long closerId;
5 private Long creatorId;
6 private String description;
7 private Long id;
8 private TaskResolution resolution;
9 private TaskStatus status;
10 private String title;
11
12 //Getters and setters are omitted
13}
Next, we will write unit tests which ensure that the showTask() method is working as expected when the requested task is found. We can write these tests by following these steps:
First, we have to add the required class hierarchy into the TaskCrudControllerTest class. After we have added this class hierarchy into our test class, its source code looks as follows:
1package com.testwithspring.starter.spring.web;
2import de.bechte.junit.runners.context.HierarchicalContextRunner;
3import org.junit.Before;
4import org.junit.Test;
5import org.junit.experimental.categories.Category;
6import org.junit.runner.RunWith;
7import org.springframework.context.support.StaticMessageSource;
8import org.springframework.test.web.servlet.MockMvc;
9import org.springframework.test.web.servlet.setup.MockMvcBuilders;
10
11import static org.mockito.Mockito.mock;
12
13@RunWith(HierarchicalContextRunner.class)
14@Category(UnitTest.class)
15public class TaskCrudControllerTest {
16
17 private TaskCrudService crudService;
18 private StaticMessageSource messageSource;
19 private MockMvc mockMvc;
20
21 @Before
22 public void configureSystemUnderTest() {
23 crudService = mock(TaskCrudService.class);
24 messageSource = new StaticMessageSource();
25
26 TaskCrudController controller = new TaskCrudController(crudService,
27 messageSource
28 );
29 mockMvc = MockMvcBuilders.standaloneSetup(controller)
30 .build();
31 }
32
33 public class ShowTask {
34
35 public class WhenTaskIsFound {
36
37 //Add tests here
38 }
39 }
40}
Second, we have to configure our test cases by following these steps:
- Add a setup method into the
WhenTaskIsFoundclass. We will configure our test cases in this method. - Create a new
TaskDTOobject. We will create this object by using a test data builder because this DTO is not just a data container. Its content depends from the state of the returned task and that is why it is a good idea to use a test data builder. - Ensure that the
TaskCrudServicemock returns the created task when itsfindById()method is invoked and1Lis passed as a method parameter.
Additional Reading:
After we have configured our test cases, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.Test;
4import org.junit.experimental.categories.Category;
5import org.junit.runner.RunWith;
6import org.springframework.context.support.StaticMessageSource;
7import org.springframework.test.web.servlet.MockMvc;
8import org.springframework.test.web.servlet.setup.MockMvcBuilders;
9
10import static org.mockito.BDDMockito.given;
11import static org.mockito.Mockito.mock;
12
13@RunWith(HierarchicalContextRunner.class)
14@Category(UnitTest.class)
15public class TaskCrudControllerTest {
16
17 private TaskCrudService crudService;
18 private StaticMessageSource messageSource;
19 private MockMvc mockMvc;
20
21 @Before
22 public void configureSystemUnderTest() {
23 crudService = mock(TaskCrudService.class);
24 messageSource = new StaticMessageSource();
25
26 TaskCrudController controller = new TaskCrudController(crudService,
27 messageSource
28 );
29 mockMvc = MockMvcBuilders.standaloneSetup(controller)
30 .build();
31 }
32
33 public class ShowTask {
34
35 public class WhenTaskIsFound {
36
37 @Before
38 public void returnFoundTask() {
39 TaskDTO found = new TaskDTOBuilder()
40 .withId(1L)
41 .withCreator(99L)
42 .withTitle("title")
43 .withDescription("description")
44 .withStatusOpen()
45 .build();
46
47 given(crudService.findById(1L)).willReturn(found);
48 }
49 }
50 }
51}
Third, we have to write three unit tests which ensure that the showTask() method is working as expected when we send a GET request to the path ‘/task/1’:
- We have to verify that the HTTP status code 200 is returned.
- We have to ensure that the
showTask()method renders the view that shows the information of a single task. - We have to verify that the correct information is shown.
After we have written these unit tests, the source code of our test class looks as follows:
1import de.bechte.junit.runners.context.HierarchicalContextRunner;
2import org.junit.Before;
3import org.junit.Test;
4import org.junit.experimental.categories.Category;
5import org.junit.runner.RunWith;
6import org.springframework.context.support.StaticMessageSource;
7import org.springframework.test.web.servlet.MockMvc;
8import org.springframework.test.web.servlet.setup.MockMvcBuilders;
9
10import static org.hamcrest.Matchers.allOf;
11import static org.hamcrest.Matchers.hasProperty;
12import static org.hamcrest.Matchers.is;
13import static org.hamcrest.Matchers.nullValue;
14import static org.mockito.BDDMockito.given;
15import static org.mockito.Mockito.mock;
16import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
17import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
18import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
20
21@RunWith(HierarchicalContextRunner.class)
22@Category(UnitTest.class)
23public class TaskCrudControllerTest {
24
25 private TaskCrudService crudService;
26 private StaticMessageSource messageSource;
27 private MockMvc mockMvc;
28
29 @Before
30 public void configureSystemUnderTest() {
31 crudService = mock(TaskCrudService.class);
32 messageSource = new StaticMessageSource();
33
34 TaskCrudController controller = new TaskCrudController(crudService,
35 messageSource
36 );
37 mockMvc = MockMvcBuilders.standaloneSetup(controller)
38 .build();
39 }
40
41 public class ShowTask {
42
43 public class WhenTaskIsFound {
44
45 @Before
46 public void returnFoundTask() {
47 TaskDTO found = new TaskDTOBuilder()
48 .withId(1L)
49 .withCreator(99L)
50 .withTitle("title")
51 .withDescription("description")
52 .withStatusOpen()
53 .build();
54
55 given(crudService.findById(1L)).willReturn(found);
56 }
57
58 @Test
59 public void shouldReturnHttpStatusCodeOk() throws Exception {
60 mockMvc.perform(get("/task/{taskId}", 1L))
61 .andExpect(status().isOk());
62 }
63
64 @Test
65 public void shouldRenderViewTaskView() throws Exception {
66 mockMvc.perform(get("/task/{taskId}", 1L))
67 .andExpect(view().name("task/view"));
68 }
69
70 @Test
71 public void shouldShowFoundTask() throws Exception {
72 mockMvc.perform(get("/task/{taskId}", 1L))
73 .andExpect(model().attribute("task", allOf(
74 hasProperty("assigneeId", nullValue()),
75 hasProperty("closerId", nullValue()),
76 hasProperty("creatorId", is(99L)),
77 hasProperty("id", is(1L)),
78 hasProperty("title", is("title")),
79 hasProperty("description", is("description")),
80 hasProperty("status", is(TaskStatus.OPEN)),
81 hasProperty("resolution", nullValue())
82 )));
83 }
84 }
85 }
86}
Additional Reading:
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- We can create HTTP requests by using the
staticfactory methods provided by theMockMvcRequestBuildersclass. - We can write assertions for the returned response by using the
staticfactory methods provided by theMockMvcResultMatchersclass. - We can write complex assertions by combining different Hamcrest matchers.