Writing Unit Tests for "Normal" Spring MVC Controllers - Rendering Data
After we have finished this lesson, we:
- Can create feature methods which specify the expected behavior of “normal” Spring MVC controllers that render data.
- Understand how we can document the system under specification by using the
setup,when, andthenblocks.
This lesson assumes that:
Introduction to System Under Specification
We have to specify the expected behavior of the controller method that processes GET requests send to the path: ‘/task/{taskId}’. The name of this method is 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 system under specification 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 data transfer object that contains the information of a single task. Its source code looks as follows:
1import java.time.ZonedDateTime;
2import java.util.ArrayList;
3import java.util.List;
4
5public class TaskDTO {
6
7 private PersonDTO assignee;
8 private PersonDTO closer;
9 private ZonedDateTime creationTime;
10 private PersonDTO creator;
11 private String description;
12 private Long id;
13 private ZonedDateTime modificationTime;
14 private PersonDTO modifier;
15 private TaskResolution resolution;
16 private TaskStatus status;
17 private List<TagDTO> tags = new ArrayList<>();
18 private String title;
19
20 //Getters and setters are omitted
21}
The PersonDTO class is a data transfer object that contains the information of a single person. Its source code looks as follows:
1public class PersonDTO {
2
3 private String name;
4 private Long userId;
5
6 //Getters and setters are omitted
7}
The TagDTO class is a data transfer object that contains the information of a single tag. Its source code looks as follows:
1public class TagDTO {
2
3 private Long id;
4 private String name;
5
6 //Getters and setters are omitted
7}
Next, we will find out how we can write the feature method which specifies the expected behavior of the system under specification.
Specifying the Expected Behavior
We can write the feature method which specifies the behavior of the showTask() method by following these steps:
First, we have to add a new feature method to our specification class and define a variable that contains the response (a ResultActions object) returned by the tested controller method.
After we have added a new feature method to our specification class, the source code of our specification class looks as follows:
1import org.junit.experimental.categories.Category
2import org.springframework.context.support.StaticMessageSource
3import org.springframework.test.web.servlet.setup.MockMvcBuilders
4import spock.lang.Specification
5
6import java.time.ZonedDateTime
7
8import static com.testwithspring.master.web.WebTestConfig.exceptionResolver
9import static com.testwithspring.master.web.WebTestConfig.fixedLocaleResolver
10import static com.testwithspring.master.web.WebTestConfig.jspViewResolver
11
12@Category(UnitTest.class)
13class TaskCrudControllerSpec extends Specification {
14
15 private static final CREATION_TIME = ZonedDateTime.now().minusDays(4)
16 private static final MODIFICATION_TIME = CREATION_TIME.plusDays(2)
17
18 def messageSource = new StaticMessageSource()
19 def service = Mock(TaskCrudService)
20 def mockMvc = MockMvcBuilders
21 .standaloneSetup(new TaskCrudController(service, messageSource))
22 .setHandlerExceptionResolvers(exceptionResolver())
23 .setLocaleResolver(fixedLocaleResolver())
24 .setViewResolvers(jspViewResolver())
25 .build()
26
27 def 'Show task'() {
28
29 def response
30 }
31}
Second, we have to verify that the showTask() method is working as expected when the requested task is not found. We can add the required when and then blocks to our feature method by following these steps:
- Ensure that the
TaskCrudServicemock throws aNotFoundExceptionwhen itsfindById()method is invoked by using the method parameter:99L. - Send a
GETrequest to the path: ‘/task/99’ and store the returnedResultActionsobject in theresponsevariable. - Verify that the tested controller method returns the HTTP status code 404.
- Ensure that the tested controller method renders the 404 view.
After we have written the required when and then blocks, the source code of our specification class looks as follows:
1import org.junit.experimental.categories.Category
2import org.springframework.context.support.StaticMessageSource
3import org.springframework.test.web.servlet.setup.MockMvcBuilders
4import spock.lang.Specification
5
6import java.time.ZonedDateTime
7
8import static com.testwithspring.master.web.WebTestConfig.exceptionResolver
9import static com.testwithspring.master.web.WebTestConfig.fixedLocaleResolver
10import static com.testwithspring.master.web.WebTestConfig.jspViewResolver
11import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
12import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
13import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view
14
15@Category(UnitTest.class)
16class TaskCrudControllerSpec extends Specification {
17
18 private static final CREATION_TIME = ZonedDateTime.now().minusDays(4)
19 private static final MODIFICATION_TIME = CREATION_TIME.plusDays(2)
20
21 def messageSource = new StaticMessageSource()
22 def service = Mock(TaskCrudService)
23 def mockMvc = MockMvcBuilders
24 .standaloneSetup(new TaskCrudController(service, messageSource))
25 .setHandlerExceptionResolvers(exceptionResolver())
26 .setLocaleResolver(fixedLocaleResolver())
27 .setViewResolvers(jspViewResolver())
28 .build()
29
30 def 'Show task'() {
31
32 def response
33
34 when: 'No task is found'
35 service.findById(99L) >> {
36 throw new NotFoundException('')
37 }
38
39 and: 'A user tries to open the view task page'
40 response = mockMvc.perform(get('/task/{taskId}', 99L))
41
42 then: 'Should return the HTTP status code not found'
43 response.andExpect(status().isNotFound())
44
45 and: 'Should render the 404 view'
46 response.andExpect(view().name('error/404'))
47 }
48}
Third, we have to verify that the showTask() method is working as expected when the requested task is found. We can add the required when and then blocks to our feature method by following these steps:
- Ensure that the
TaskCrudServicemock returns a closed task that has one tag when itsfindById()method is invoked by using the method parameter:1L. - Send a
GETrequest to the path: ‘/task/1’ and store the returnedResultActionsobject in theresponsevariable. - Verify that the tested controller method returns the HTTP status code 200.
- Ensure that the tested controller method renders the view task view.
- Verify that the tested controller method displays the information of the found task.
- Ensure that the tested controller method displays the information of one tag.
- Verify that the tested controller method displays the information of the correct tag.
After we have written the required when and then blocks, the source code of our specification class looks as follows:
1import org.junit.experimental.categories.Category
2import org.springframework.context.support.StaticMessageSource
3import org.springframework.test.web.servlet.setup.MockMvcBuilders
4import spock.lang.Specification
5
6import java.time.ZonedDateTime
7
8import static com.testwithspring.master.web.WebTestConfig.exceptionResolver
9import static com.testwithspring.master.web.WebTestConfig.fixedLocaleResolver
10import static com.testwithspring.master.web.WebTestConfig.jspViewResolver
11import static org.hamcrest.Matchers.*
12import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
13import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model
14import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
15import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view
16
17@Category(UnitTest.class)
18class TaskCrudControllerSpec extends Specification {
19
20 private static final CREATION_TIME = ZonedDateTime.now().minusDays(4)
21 private static final MODIFICATION_TIME = CREATION_TIME.plusDays(2)
22
23 def messageSource = new StaticMessageSource()
24 def service = Mock(TaskCrudService)
25 def mockMvc = MockMvcBuilders
26 .standaloneSetup(new TaskCrudController(service, messageSource))
27 .setHandlerExceptionResolvers(exceptionResolver())
28 .setLocaleResolver(fixedLocaleResolver())
29 .setViewResolvers(jspViewResolver())
30 .build()
31
32 def 'Show task'() {
33
34 def response
35
36 when: 'No task is found'
37 service.findById(99L) >> {
38 throw new NotFoundException('')
39 }
40
41 and: 'A user tries to open the view task page'
42 response = mockMvc.perform(get('/task/{taskId}', 99L))
43
44 then: 'Should return the HTTP status code not found'
45 response.andExpect(status().isNotFound())
46
47 and: 'Should render the 404 view'
48 response.andExpect(view().name('error/404'))
49
50 when: 'A closed task is found and it has one tag'
51 service.findById(1L) >> {
52 new TaskDTOBuilder()
53 .withId(1L)
54 .withAssignee(new PersonDTO(
55 userId: 44L,
56 name: 'Anne Assignee'
57 ))
58 .withCreationTime(CREATION_TIME)
59 .withCreator(new PersonDTO(
60 userId: 99L,
61 name: 'John Doe'
62 ))
63 .withDescription('description')
64 .withModificationTime(MODIFICATION_TIME)
65 .withModifier(new PersonDTO(
66 userId: 33L,
67 name: 'Jane Doe'
68 ))
69 .withResolutionDone(new PersonDTO(
70 userId: 931L,
71 name: 'Chris Closer'
72 ))
73 .withTags(new TagDTO(id: 33L, name: 'testing'))
74 .withTitle('title')
75 .build()
76 }
77
78 and: 'A user opens the view task page'
79 response = mockMvc.perform(get('/task/{taskId}', 1L))
80
81 then: 'Should return the HTTP status code OK'
82 response.andExpect(status().isOk())
83
84 and: 'Should render the view task view'
85 response.andExpect(view().name('task/view'))
86
87 and: 'Should display the information of the found task'
88 response.andExpect(model().attribute('task', allOf(
89 hasProperty('assignee', allOf(
90 hasProperty('name', is('Anne Assignee')),
91 hasProperty('userId', is(44L))
92 )),
93 hasProperty('closer', allOf(
94 hasProperty('name', is('Chris Closer')),
95 hasProperty('userId', is(931L))
96 )),
97 hasProperty('creationTime', is(CREATION_TIME)),
98 hasProperty('creator', allOf(
99 hasProperty('name', is('John Doe')),
100 hasProperty('userId', is(99L))
101 )),
102 hasProperty('id', is(1L)),
103 hasProperty('modificationTime', is(MODIFICATION_TIME)),
104 hasProperty('modifier', allOf(
105 hasProperty('name', is('Jane Doe')),
106 hasProperty('userId', is(33L))
107 )),
108 hasProperty('title', is('title')),
109 hasProperty('description', is('description')),
110 hasProperty('status', is(TaskStatus.CLOSED)),
111 hasProperty('resolution', is(TaskResolution.DONE))
112 )))
113
114 and: 'Should display one tag of the found task'
115 response.andExpect(model().attribute('task',
116 hasProperty('tags', hasSize(1))
117 ))
118
119 and: 'Should display the tag of the found task'
120 response.andExpect(model().attribute('task',
121 hasProperty('tags', contains(
122 allOf(
123 hasProperty('id', is(33L)),
124 hasProperty('name', is('testing'))
125 )
126 ))
127 ))
128 }
129}
The TaskDTOBuilder class is a test data builder that creates new TaskDTO objects. We use a test data builder because the TaskDTO class is not just a data container, and we don’t want to put the object creation logic to our specification classes.
Also, if this feature method looks strange to you, you should take a look at the following lesson:
This lesson describes how you can write unit tests for a controller method that renders data. Also, it provides additional material that was left out from this lesson.
Finally, there are three things I want to point out:
- Our feature method has two test cases that use different
whenandthenblocks. This allows us to identify the tested feature and specify how the feature should behave in different situations. - We have divided the
whenandthenblocks into different sections by using theandlabel and each section has a specific responsibility. For example, it might invoke the tested controller method or specify the condition which verifies that the tested controller method renders the correct view. - If we add good descriptions to our feature method, our feature method becomes an executable specification that documents the behavior of the system under specification.
We can now write unit tests for controller methods that render data. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- We can test different scenarios by adding
whenandthenblocks to our feature method. - If a block has multiple responsibilities, we should divide it into different sections by using the
andlabel. - We should add good descriptions to our feature method because this allows us to create executable specifications that document the behavior of the system under specification.