Writing Unit Tests for "Normal" Controllers - Forms
After we have finished this lesson, we
- Know how we can “submit a form” by using the Spring MVC Test framework.
- Understand how we can verify that a form field has validation errors.
- Know how can verify that the request is redirected to the specified path.
- Know how we can write assertions for the flash messages shown to the user.
This lesson assumes that:
Introduction to System Under Test
We have to write unit tests for a controller method that processes POST requests send to the path: ‘/task/create’. The name of this method is processCreateTaskForm(), and it is implemented by following these steps:
- If the submitted form has validation errors, return the name of the form view (’task/create’).
- Save the created task to the database by invoking the
create()method of theTaskCrudServiceinterface. - Create a feedback message which states that a new task was created and ensure that this message is shown when the next view is rendered.
- Redirect the request to the view that renders the information of the created task.
The source code of the tested controller method looks as follows:
1import com.testwithspring.starter.spring.task.TaskCrudService;
2import com.testwithspring.starter.spring.task.TaskDTO;
3import com.testwithspring.starter.spring.task.TaskFormDTO;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.context.MessageSource;
6import org.springframework.stereotype.Controller;
7import org.springframework.validation.BindingResult;
8import org.springframework.web.bind.annotation.ModelAttribute;
9import org.springframework.web.bind.annotation.RequestMapping;
10import org.springframework.web.bind.annotation.RequestMethod;
11import org.springframework.web.servlet.mvc.support.RedirectAttributes;
12
13import javax.validation.Valid;
14import java.util.Locale;
15
16@Controller
17public class TaskCrudController {
18
19 private final TaskCrudService crudService;
20 private final MessageSource messageSource;
21
22 @Autowired
23 TaskCrudController(TaskCrudService crudService, MessageSource messageSource) {
24 this.crudService = crudService;
25 this.messageSource = messageSource;
26 }
27
28 @RequestMapping(value = "/task/create", method = RequestMethod.POST)
29 public String processCreateTaskForm(@Valid @ModelAttribute("task") TaskFormDTO formObject,
30 BindingResult bindingResult,
31 RedirectAttributes redirectAttributes,
32 Locale currentLocale) {
33 if (bindingResult.hasErrors()) {
34 return "task/create";
35 }
36
37 TaskDTO created = crudService.create(formObject);
38
39 addFeedbackMessage(redirectAttributes,
40 "feedback.message.task.created",
41 currentLocale,
42 created.getTitle()
43 );
44
45 redirectAttributes.addAttribute("taskId", created.getId());
46 return "redirect:/task/{taskId}";
47 }
48
49 private void addFeedbackMessage(RedirectAttributes attributes,
50 String messageCode,
51 Locale currentLocale,
52 Object... messageParameters) {
53 String feedbackMessage = messageSource.getMessage(messageCode,
54 messageParameters,
55 currentLocale
56 );
57 attributes.addFlashAttribute("feedbackMessage", feedbackMessage);
58 }
59}
The TaskFormDTO class contains the information of the form object that is used to create new tasks and update the information of existing tasks. It also declares the validation rules that are used to validate the form object. The source code of the TaskFormDTO class looks as follows:
1import org.hibernate.validator.constraints.NotBlank;
2
3import javax.validation.constraints.Size;
4
5public class TaskFormDTO {
6
7 private Long id;
8
9 @Size(max = 500)
10 private String description;
11
12 @NotBlank
13 @Size(max = 100)
14 private String title;
15
16 public TaskFormDTO() {
17
18 }
19}
We are now ready to write unit tests for this controller method. Let’s start by configuring the system under test.
Configuring the System Under Test
We can 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 earlier
, I won’t repeat myself here. However, I want to point out that this time we will add a custom HandlerExceptionResolver and a custom LocaleResolver 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 com.testwithspring.starter.spring.web.WebTestConfig.exceptionResolver;
10import static com.testwithspring.starter.spring.web.WebTestConfig.fixedLocaleResolver;
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 .setHandlerExceptionResolvers(exceptionResolver())
31 .setLocaleResolver(fixedLocaleResolver())
32 .build();
33 }
34}
Next, we will write unit tests which ensure that the tested controller method is working as excepted when validation fails.
Scenario 1: Validation Fails
Now we will write unit tests which verify that the tested controller method is working as expected when an empty form is submitted. We can write these unit tests by following these steps:
First, 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 this 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 com.testwithspring.starter.spring.web.WebTestConfig.exceptionResolver;
10import static com.testwithspring.starter.spring.web.WebTestConfig.fixedLocaleResolver;
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 .setHandlerExceptionResolvers(exceptionResolver())
31 .setLocaleResolver(fixedLocaleResolver())
32 .build();
33 }
34
35 public class ProcessCreateTaskForm {
36
37 public class WhenValidationFails {
38
39 public class WhenEmptyFormIsSubmitted {
40
41 }
42 }
43 }
44}
Second, we have to write six unit tests for the processCreateTaskForm() 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 create new task view.
- We have to verify that it shows a validation error about an empty title.
- We have to ensure that empty title and description are shown on the create new task form.
- We have to verify that the value of the
idproperty is not set. - We have to ensure that a new task was not created.
After we have written these unit tests, the source code of 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 com.testwithspring.starter.spring.web.WebTestConfig.exceptionResolver;
11import static com.testwithspring.starter.spring.web.WebTestConfig.fixedLocaleResolver;
12import static org.hamcrest.Matchers.allOf;
13import static org.hamcrest.Matchers.hasProperty;
14import static org.hamcrest.Matchers.is;
15import static org.hamcrest.Matchers.nullValue;
16import static org.mockito.Matchers.isA;
17import static org.mockito.Mockito.mock;
18import static org.mockito.Mockito.never;
19import static org.mockito.Mockito.verify;
20import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
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 .setHandlerExceptionResolvers(exceptionResolver())
43 .setLocaleResolver(fixedLocaleResolver())
44 .build();
45 }
46
47 public class ProcessCreateTaskForm {
48
49 public class WhenValidationFails {
50
51 public class WhenEmptyFormIsSubmitted {
52
53 @Test
54 public void shouldReturnHttpStatusCodeOk() throws Exception {
55 mockMvc.perform(post("/task/create")
56 .param("description", "")
57 .param("title", "")
58 )
59 .andExpect(status().isOk());
60 }
61
62 @Test
63 public void shouldRenderCreateTaskView() throws Exception {
64 mockMvc.perform(post("/task/create")
65 .param("description", "")
66 .param("title", "")
67 )
68 .andExpect(view().name("task/create"));
69 }
70
71 @Test
72 public void shouldShowValidationErrorForEmptyTitle() throws Exception {
73 mockMvc.perform(post("/task/create")
74 .param("description", "")
75 .param("title", "")
76 )
77 .andExpect(
78 model().attributeHasFieldErrorCode("task",
79 "title",
80 is("NotBlank")
81 )
82 );
83 }
84
85 @Test
86 public void shouldShowEmptyTitleAndDescription() throws Exception {
87 mockMvc.perform(post("/task/create")
88 .param("description", "")
89 .param("title", "")
90 )
91 .andExpect(model().attribute("task", allOf(
92 hasProperty("description", is("")),
93 hasProperty("title", is(""))
94 )));
95 }
96
97 @Test
98 public void shouldNotModifyIdField() throws Exception {
99 mockMvc.perform(post("/task/create")
100 .param("description", "")
101 .param("title", "")
102 )
103 .andExpect(model().attribute("task",
104 hasProperty("id", nullValue())
105 ));
106 }
107
108 @Test
109 public void shouldNotCreateNewTask() throws Exception {
110 mockMvc.perform(post("/task/create")
111 .param("description", "")
112 .param("title", "")
113 );
114
115 verify(crudService, never())
116 .create(isA(TaskFormDTO.class));
117 }
118 }
119 }
120 }
121}
Additional Reading:
Let’s move on and find out how we can write unit tests which ensure that the tested controller method is working as expected when no validation errors is found.
Scenario 2: Validation is Successful
Now we will write unit tests which verify that the tested controller method is working as expected when the create new task form is submitted with the following information:
- The title of the created task is: ’title’.
- The description of the created task is: ‘description’.
We can write these unit 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:
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 com.testwithspring.starter.spring.web.WebTestConfig.exceptionResolver;
11import static com.testwithspring.starter.spring.web.WebTestConfig.fixedLocaleResolver;
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 .setHandlerExceptionResolvers(exceptionResolver())
32 .setLocaleResolver(fixedLocaleResolver())
33 .build();
34 }
35
36 public class ProcessCreateTaskForm {
37
38 public class WhenValidationIsSuccessful {
39
40 }
41 }
42}
Second, we have to configure our test cases by following these steps:
- Add a
private returnFeedbackMessage()method into theWhenValidationIsSuccessfulclass. This method configures the feedback message that is shown to the user after a new task has been created. - Add a
private returnCreatedTask()method into theWhenValidationIsSuccessfulclass. This method creates the returnedTaskDTOobject by using a test data builder and ensures that theTaskCrudServicemock returns the created object when itscreate()method is invoked and aTaskFormDTOobject is passed as a method parameter. - Add a setup method into the
WhenValidationIsSuccessfulclass and configure our test cases by invoking both private methods.
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 static com.testwithspring.starter.spring.web.WebTestConfig.exceptionResolver;
10import static com.testwithspring.starter.spring.web.WebTestConfig.fixedLocaleResolver;
11import static org.mockito.BDDMockito.given;
12import static org.mockito.Matchers.isA;
13import static org.mockito.Mockito.mock;
14
15@RunWith(HierarchicalContextRunner.class)
16@Category(UnitTest.class)
17public class TaskCrudControllerTest {
18
19 private TaskCrudService crudService;
20 private StaticMessageSource messageSource;
21 private MockMvc mockMvc;
22
23 @Before
24 public void configureSystemUnderTest() {
25 crudService = mock(TaskCrudService.class);
26 messageSource = new StaticMessageSource();
27
28 TaskCrudController controller = new TaskCrudController(crudService,
29 messageSource
30 );
31 mockMvc = MockMvcBuilders.standaloneSetup(controller)
32 .setHandlerExceptionResolvers(exceptionResolver())
33 .setLocaleResolver(fixedLocaleResolver())
34 .build();
35 }
36
37 public class ProcessCreateTaskForm {
38
39 public class WhenValidationIsSuccessful {
40
41 @Before
42 public void configureTestCases() {
43 returnCreatedTask();
44 returnFeedbackMessage();
45 }
46
47 private void returnCreatedTask() {
48 TaskDTO created = new TaskDTOBuilder()
49 .withId(1L)
50 .withCreator(99L)
51 .withTitle("title")
52 .withDescription("description")
53 .withStatusOpen()
54 .build();
55 given(crudService.create(isA(TaskFormDTO.class)))
56 .willReturn(created);
57 }
58
59 private void returnFeedbackMessage() {
60 messageSource.addMessage("feedback.message.task.created",
61 WebTestConfig.LOCALE,
62 "Task created"
63 );
64 }
65 }
66 }
67}
We create the returned TaskDTO object by using a test data builder because the returned object 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.
Additional Reading:
Third, we have to write six unit tests for the processCreateTaskForm() method of the TaskCrudController class:
- We have to verify that it returns the HTTP status code 302.
- We have to ensure that it redirects the request to the view that renders the information of the created task.
- We have to verify that it creates the feedback message that is shown to the user when the view that contains the information of the created task is rendered.
- We have to ensure that it creates a new task that has the correct description.
- We have to verify that it creates a new task that has no id.
- We have to ensure that it creates a new task that has the correct title.
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 com.testwithspring.starter.spring.web.WebTestConfig.exceptionResolver;
11import static com.testwithspring.starter.spring.web.WebTestConfig.fixedLocaleResolver;
12import static info.solidsoft.mockito.java8.AssertionMatcher.assertArg;
13import static org.assertj.core.api.Assertions.assertThat;
14import static org.hamcrest.Matchers.is;
15import static org.mockito.BDDMockito.given;
16import static org.mockito.Matchers.isA;
17import static org.mockito.Mockito.mock;
18import static org.mockito.Mockito.times;
19import static org.mockito.Mockito.verify;
20import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
21import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.flash;
22import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
24import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
25
26@RunWith(HierarchicalContextRunner.class)
27@Category(UnitTest.class)
28public class TaskCrudControllerTest {
29
30 private TaskCrudService crudService;
31 private StaticMessageSource messageSource;
32 private MockMvc mockMvc;
33
34 @Before
35 public void configureSystemUnderTest() {
36 crudService = mock(TaskCrudService.class);
37 messageSource = new StaticMessageSource();
38
39 TaskCrudController controller = new TaskCrudController(crudService,
40 messageSource
41 );
42 mockMvc = MockMvcBuilders.standaloneSetup(controller)
43 .setHandlerExceptionResolvers(exceptionResolver())
44 .setLocaleResolver(fixedLocaleResolver())
45 .build();
46 }
47
48 public class ProcessCreateTaskForm {
49
50 public class WhenValidationIsSuccessful {
51
52 @Before
53 public void configureTestCases() {
54 returnCreatedTask();
55 returnFeedbackMessage();
56 }
57
58 private void returnCreatedTask() {
59 TaskDTO created = new TaskDTOBuilder()
60 .withId(1L)
61 .withCreator(99L)
62 .withTitle("title")
63 .withDescription("description")
64 .withStatusOpen()
65 .build();
66 given(crudService.create(isA(TaskFormDTO.class)))
67 .willReturn(created);
68 }
69
70 private void returnFeedbackMessage() {
71 messageSource.addMessage("feedback.message.task.created",
72 WebTestConfig.LOCALE,
73 "Task created"
74 );
75 }
76
77 @Test
78 public void shouldReturnHttpStatusCodeFound() throws Exception {
79 mockMvc.perform(post("/task/create")
80 .param("description", "description")
81 .param("title", "title")
82 )
83 .andExpect(status().isFound());
84 }
85
86 @Test
87 public void shouldRedirectUserToViewTaskView() throws Exception {
88 mockMvc.perform(post("/task/create")
89 .param("description", "description")
90 .param("title", "title")
91 )
92 .andExpect(view().name("redirect:/task/{taskId}"))
93 .andExpect(model().attribute("taskId", is("1")));
94 }
95
96 @Test
97 public void shouldAddFeedbackMessageAsAFlashAttribute() throws Exception {
98 mockMvc.perform(post("/task/create")
99 .param("description", "description")
100 .param("title", "title")
101 )
102 .andExpect(flash().attribute("feedbackMessage",
103 "Task created"
104 ));
105 }
106
107 @Test
108 public void shouldCreateNewTaskWithCorrectDescription() throws Exception {
109 mockMvc.perform(post("/task/create")
110 .param("description", "description")
111 .param("title", "title")
112 );
113
114 verify(crudService, times(1)).create(assertArg(
115 task -> assertThat(task.getDescription())
116 .isEqualTo("description")
117 ));
118 }
119
120 @Test
121 public void shouldCreateNewTaskWithNullId() throws Exception {
122 mockMvc.perform(post("/task/create")
123 .param("description", "description")
124 .param("title", "title")
125 );
126
127 verify(crudService, times(1)).create(assertArg(
128 task -> assertThat(task.getId()).isNull()
129 ));
130 }
131
132 @Test
133 public void shouldCreateNewTaskWithCorrectTitle() throws Exception {
134 mockMvc.perform(post("/task/create")
135 .param("description", "description")
136 .param("title", "title")
137 );
138
139 verify(crudService, times(1)).create(assertArg(
140 task -> assertThat(task.getTitle()).isEqualTo("title")
141 ));
142 }
143 }
144 }
145}
Additional Reading:
Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us four things:
- We can configure the value of a form field by using the
param()method of theMockHttpServletRequestBuilderclass. - We can verify that a form field has a specific validation error by using the
attributeHasFieldErrorCode()method of theModelResultMatchersclass. - We can create the “name” of the path in which the request is redirected by prepending the string: ‘redirect:’ to the expected path.
- We can write assertions for the flash messages by using the
attribute()method of theFlashAttributeResultMatchersclass.