Writing Integration Tests for "Normal" Spring MVC Controllers - Rendering Data
After we have finished this lesson, we
- Know how we can write integration tests for normal Spring MVC controllers that render data.
- Understand how we should structure our specification classes when we specify the behavior of the system under specification.
This lesson assumes that:
- You are familiar with DbUnit
- You can configure your integration tests which use JUnit 4.
- You can write integration tests for Spring web applications by using JUnit 4.
- You are familiar with Spock Framework
- You can configure the system under specification when you write integration tests with Spock Framework
- You can use DbUnit with Spock Framework
Introduction to System Under Specification
We have to specify the 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.
- 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 com.testwithspring.intermediate.task.TaskCrudService;
2import com.testwithspring.intermediate.task.TaskDTO;
3import org.springframework.beans.factory.annotation.Autowired;
4import org.springframework.context.MessageSource;
5import org.springframework.stereotype.Controller;
6import org.springframework.ui.Model;
7import org.springframework.web.bind.annotation.PathVariable;
8import org.springframework.web.bind.annotation.RequestMapping;
9import org.springframework.web.bind.annotation.RequestMethod;
10
11@Controller
12public class TaskCrudController {
13
14 private final TaskCrudService crudService;
15 private final MessageSource messageSource;
16
17 @Autowired
18 TaskCrudController(TaskCrudService crudService,
19 MessageSource messageSource) {
20 this.crudService = crudService;
21 this.messageSource = messageSource;
22 }
23
24 @RequestMapping(value = "/task/{taskId}", method = RequestMethod.GET)
25 public String showTask(@PathVariable("taskId") Long taskId, Model model) {
26 TaskDTO task = crudService.findById(taskId);
27 model.addAttribute("task", task);
28 return "task/view";
29 }
30}
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 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 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 specify the expected behavior of the system under specification.
Specifying the Expected Behavior
We can specify the behavior of the system under specification by following these steps:
First, we have to create a new specification class and configure the system under specification. Also, because we are writing integration tests for a Spring MVC controller, we have to configure the Spring MVC Test framework.
After we have configured the system under specification, the source code of our specification class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DbUnitConfiguration
3import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
4import org.junit.experimental.categories.Category
5import org.springframework.beans.factory.annotation.Autowired
6import org.springframework.test.context.ActiveProfiles
7import org.springframework.test.context.ContextConfiguration
8import org.springframework.test.context.TestExecutionListeners
9import org.springframework.test.context.web.WebAppConfiguration
10import org.springframework.test.web.servlet.setup.MockMvcBuilders
11import org.springframework.web.context.WebApplicationContext
12import spock.lang.Specification
13
14import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
15
16@ContextConfiguration(classes = [IntegrationTestContext.class])
17@WebAppConfiguration
18@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
19 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
20)
21@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
22@Category(IntegrationTest.class)
23@ActiveProfiles(Profiles.INTEGRATION_TEST)
24class ShowTaskSpec extends Specification {
25
26 @Autowired
27 WebApplicationContext webAppContext
28
29 def mockMvc
30
31 def setup() {
32 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
33 .apply(springSecurity())
34 .build()
35 }
36}
Second, we have to initialize our database into a known state before our feature methods are run. Because we want that all feature methods of our specification class use the same data sets, we have to annotate our specification class with the @DatabaseSetup annotation. When we do this, we have to specify two data sets that insert the required test data into the tasks and user_accounts tables.
After we have configured the used test data, the source code of our specification class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DatabaseSetup
3import com.github.springtestdbunit.annotation.DbUnitConfiguration
4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
5import org.junit.experimental.categories.Category
6import org.springframework.beans.factory.annotation.Autowired
7import org.springframework.test.context.ActiveProfiles
8import org.springframework.test.context.ContextConfiguration
9import org.springframework.test.context.TestExecutionListeners
10import org.springframework.test.context.web.WebAppConfiguration
11import org.springframework.test.web.servlet.setup.MockMvcBuilders
12import org.springframework.web.context.WebApplicationContext
13import spock.lang.Specification
14
15import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
16
17@ContextConfiguration(classes = [IntegrationTestContext.class])
18@WebAppConfiguration
19@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
20 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
21)
22@DatabaseSetup([
23 '/com/testwithspring/master/users.xml',
24 '/com/testwithspring/master/tasks.xml'
25])
26@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
27@Category(IntegrationTest.class)
28@ActiveProfiles(Profiles.INTEGRATION_TEST)
29class ShowTaskSpec extends Specification {
30
31 @Autowired
32 WebApplicationContext webAppContext
33
34 def mockMvc
35
36 def setup() {
37 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
38 .apply(springSecurity())
39 .build()
40 }
41}
The DbUnit data set that inserts the required test data into the user_accounts table (users.xml) looks as follows:
1<dataset>
2 <user_accounts id="1"
3 creation_time="2017-02-11 11:41:28"
4 email_address="john.doe@gmail.com"
5 is_enabled="true"
6 modification_time="2017-02-11 11:41:28"
7 name="John Doe"
8 password="user"
9 role="ROLE_USER"
10 version="0"/>
11 <user_accounts id="2"
12 creation_time="2017-02-11 11:41:28"
13 email_address="anne.admin@gmail.com"
14 is_enabled="true"
15 modification_time="2017-02-11 11:41:28"
16 name="Anne Admin"
17 password="admin"
18 role="ROLE_ADMIN"
19 version="0"/>
20</dataset>
The DbUnit data set that inserts the required test data into the tasks table (tasks.xml) looks as follows:
1<dataset>
2 <tasks id="1"
3 assignee_id="1"
4 closer_id="1"
5 creation_time="2016-12-3 11:41:28"
6 creator_id="1"
7 description="This example contains integration tests"
8 modification_time="2016-12-3 11:41:28"
9 modifier_id="1"
10 resolution="DONE"
11 status="CLOSED"
12 title="Write example application"
13 version="0"/>
14 <tasks id="2"
15 assignee_id="[null]"
16 closer_id="[null]"
17 creation_time="2016-12-4 11:41:28"
18 creator_id="1"
19 description="This lesson talks about integration testing"
20 modification_time="2016-12-4 11:41:28"
21 modifier_id="1"
22 resolution="[null]"
23 status="OPEN"
24 title="Write lesson"
25 version="0"/>
26
27 <tags id="1"
28 creation_time="2016-12-3 11:41:28"
29 modification_time="2016-12-3 11:41:28"
30 name="example"
31 version="0"/>
32 <tags id="2"
33 creation_time="2016-12-3 11:41:28"
34 modification_time="2016-12-3 11:41:28"
35 name="lesson"
36 version="0"/>
37
38 <tasks_tags task_id="1" tag_id="1"/>
39 <tasks_tags task_id="2" tag_id="2"/>
40</dataset>
During this lesson we will use two constant classes:
- The
Usersclass contains the information of the users found from our database. - The
Tasksclass contains the information of the tasks found from our database.
If you want to get more information about constant classes, you should take a look at the lesson: DbUnit Best Practices: Using Test Data in Our Java Code .
Third, we have to write a private method that gets the task id as a method parameter and sends a GET request to the path: ‘/task/{taskId}’. This method returns a ResultActions object, and it helps us to reduce the duplicate code which can be found from specification classes that use the Spring MVC Test framework.
After we have added this method to our specification class, its source code looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DatabaseSetup
3import com.github.springtestdbunit.annotation.DbUnitConfiguration
4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
5import org.junit.experimental.categories.Category
6import org.springframework.beans.factory.annotation.Autowired
7import org.springframework.test.context.ActiveProfiles
8import org.springframework.test.context.ContextConfiguration
9import org.springframework.test.context.TestExecutionListeners
10import org.springframework.test.context.web.WebAppConfiguration
11import org.springframework.test.web.servlet.ResultActions
12import org.springframework.test.web.servlet.setup.MockMvcBuilders
13import org.springframework.web.context.WebApplicationContext
14import spock.lang.Specification
15
16import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
17import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
18
19@ContextConfiguration(classes = [IntegrationTestContext.class])
20@WebAppConfiguration
21@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
22 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
23)
24@DatabaseSetup([
25 '/com/testwithspring/master/users.xml',
26 '/com/testwithspring/master/tasks.xml'
27])
28@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
29@Category(IntegrationTest.class)
30@ActiveProfiles(Profiles.INTEGRATION_TEST)
31class ShowTaskSpec extends Specification {
32
33 @Autowired
34 WebApplicationContext webAppContext
35
36 def mockMvc
37
38 def setup() {
39 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
40 .apply(springSecurity())
41 .build()
42 }
43
44 private ResultActions openViewTaskPage(taskId) throws Exception {
45 return mockMvc.perform(get('/task/{taskId}', taskId))
46 }
47}
Fourth, we have to write a feature method which specifies the behavior of the system under specification when an anonymous user tries to open the view task page. We can write this feature method by following these steps:
- Add a new feature method to our specification class.
- Define a variable that contains the response (a
ResultActionsobject) returned by the tested controller method. - 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 302.
- Ensure that the tested controller method redirects the user to the login page.
After we have written this feature method, the source code of our specification class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DatabaseSetup
3import com.github.springtestdbunit.annotation.DbUnitConfiguration
4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
5import com.testwithspring.master.IntegrationTest
6import com.testwithspring.master.IntegrationTestContext
7import com.testwithspring.master.Tasks
8import com.testwithspring.master.config.Profiles
9import org.junit.experimental.categories.Category
10import org.springframework.beans.factory.annotation.Autowired
11import org.springframework.test.context.ActiveProfiles
12import org.springframework.test.context.ContextConfiguration
13import org.springframework.test.context.TestExecutionListeners
14import org.springframework.test.context.web.WebAppConfiguration
15import org.springframework.test.web.servlet.ResultActions
16import org.springframework.test.web.servlet.setup.MockMvcBuilders
17import org.springframework.web.context.WebApplicationContext
18import spock.lang.Specification
19
20import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
21import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
22import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
23
24@ContextConfiguration(classes = [IntegrationTestContext.class])
25@WebAppConfiguration
26@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
27 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
28)
29@DatabaseSetup([
30 '/com/testwithspring/master/users.xml',
31 '/com/testwithspring/master/tasks.xml'
32])
33@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
34@Category(IntegrationTest.class)
35@ActiveProfiles(Profiles.INTEGRATION_TEST)
36class ShowTaskSpec extends Specification {
37
38 @Autowired
39 WebApplicationContext webAppContext
40
41 def mockMvc
42
43 def setup() {
44 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
45 .apply(springSecurity())
46 .build()
47 }
48
49 def 'Open view task task page as an anonymous user'() {
50
51 def response
52
53 when: 'An anonymous user tries to open the view task page'
54 response = openViewTaskPage(Tasks.WriteExampleApp.ID)
55
56 then: 'Should return the HTTP status code found'
57 response.andExpect(status().isFound())
58
59 and: 'Should redirect the user to the login page'
60 response.andExpect(redirectedUrl('http://localhost/user/login'))
61 }
62
63 private ResultActions openViewTaskPage(taskId) throws Exception {
64 return mockMvc.perform(get('/task/{taskId}', taskId))
65 }
66}
Fifth, we have to write a feature method which specifies the behavior of the system under specification when a registered user opens the view task page. We can write this feature method by following these steps:
- Add a new feature method to our specification class.
- Configure the authenticated user. We can do this by annotating our feature method with the
@WithUserDetailsannotation. - Define a variable that contains the response (a
ResultActionsobject) returned by the tested controller method. - 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 forwards the user to view task page.
- Ensure that the tested controller method displays the information of the found task.
- Verify that the tested controller method displays the information of one tag.
- Ensure that the tested controller method displays the information of the correct tag.
After we have written this feature method, the source code of our specification class looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener
2import com.github.springtestdbunit.annotation.DatabaseSetup
3import com.github.springtestdbunit.annotation.DbUnitConfiguration
4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
5import org.junit.experimental.categories.Category
6import org.springframework.beans.factory.annotation.Autowired
7import org.springframework.security.test.context.support.WithUserDetails
8import org.springframework.test.context.ActiveProfiles
9import org.springframework.test.context.ContextConfiguration
10import org.springframework.test.context.TestExecutionListeners
11import org.springframework.test.context.web.WebAppConfiguration
12import org.springframework.test.web.servlet.ResultActions
13import org.springframework.test.web.servlet.setup.MockMvcBuilders
14import org.springframework.web.context.WebApplicationContext
15import spock.lang.Specification
16
17import static org.hamcrest.Matchers.allOf
18import static org.hamcrest.Matchers.contains
19import static org.hamcrest.Matchers.hasProperty
20import static org.hamcrest.Matchers.hasSize
21import static org.hamcrest.Matchers.is
22import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
23import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
24import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
25
26@ContextConfiguration(classes = [IntegrationTestContext.class])
27@WebAppConfiguration
28@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
29 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
30)
31@DatabaseSetup([
32 '/com/testwithspring/master/users.xml',
33 '/com/testwithspring/master/tasks.xml'
34])
35@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
36@Category(IntegrationTest.class)
37@ActiveProfiles(Profiles.INTEGRATION_TEST)
38class ShowTaskSpec extends Specification {
39
40 @Autowired
41 WebApplicationContext webAppContext
42
43 def mockMvc
44
45 def setup() {
46 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
47 .apply(springSecurity())
48 .build()
49 }
50
51 def 'Open view task task page as an anonymous user'() {
52
53 def response
54
55 when: 'An anonymous user tries to open the view task page'
56 response = openViewTaskPage(Tasks.WriteExampleApp.ID)
57
58 then: 'Should return the HTTP status code found'
59 response.andExpect(status().isFound())
60
61 and: 'Should redirect the user to the login page'
62 response.andExpect(redirectedUrl('http://localhost/user/login'))
63 }
64
65 @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
66 def 'Open view task page as a registered user'() {
67
68 def response
69
70 when: 'The requested task is found'
71 response = openViewTaskPage(Tasks.WriteExampleApp.ID)
72
73 then: 'Should return the HTTP status code OK'
74 response.andExpect(status().isOk())
75
76 and: 'Should render the view task view'
77 response.andExpect(view().name('task/view'))
78
79 and: 'Should forward the user to the view task page'
80 response.andExpect(forwardedUrl('/WEB-INF/jsp/task/view.jsp'))
81
82 and: 'Should display the information of the found task'
83 response.andExpect(model().attribute('task', allOf(
84 hasProperty('assignee', allOf(
85 hasProperty('name',
86 is(Tasks.WriteExampleApp.Assignee.NAME)
87 ),
88 hasProperty('userId',
89 is(Tasks.WriteExampleApp.Assignee.ID)
90 )
91 )),
92 hasProperty('closer', allOf(
93 hasProperty('name',
94 is(Tasks.WriteExampleApp.Closer.NAME)
95 ),
96 hasProperty('userId',
97 is(Tasks.WriteExampleApp.Closer.ID)
98 )
99 )),
100 hasProperty('creationTime', is(Tasks.WriteExampleApp.CREATION_TIME)),
101 hasProperty('creator', allOf(
102 hasProperty('name',
103 is(Tasks.WriteExampleApp.Creator.NAME)
104 ),
105 hasProperty('userId',
106 is(Tasks.WriteExampleApp.Creator.ID)
107 )
108 )),
109 hasProperty('id', is(Tasks.WriteExampleApp.ID)),
110 hasProperty('modificationTime', is(Tasks.WriteExampleApp.MODIFICATION_TIME)),
111 hasProperty('modifier', allOf(
112 hasProperty('name',
113 is(Tasks.WriteExampleApp.Modifier.NAME)
114 ),
115 hasProperty('userId',
116 is(Tasks.WriteExampleApp.Modifier.ID)
117 )
118 )),
119 hasProperty('title', is(Tasks.WriteExampleApp.TITLE)),
120 hasProperty('description', is(Tasks.WriteExampleApp.DESCRIPTION)),
121 hasProperty('status', is(Tasks.WriteExampleApp.STATUS)),
122 hasProperty('resolution', is(Tasks.WriteExampleApp.RESOLUTION))
123 )))
124
125 and: 'Should display one tag of the found task'
126 response .andExpect(model().attribute('task',
127 hasProperty('tags', hasSize(1))
128 ))
129
130 and: 'Should display the information of the found tag'
131 response.andExpect(model().attribute('task',
132 hasProperty('tags', contains(
133 allOf(
134 hasProperty(
135 'id',
136 is(Tasks.WriteExampleApp.Tags.Example.ID)
137 ),
138 hasProperty(
139 'name',
140 is(Tasks.WriteExampleApp.Tags.Example.NAME)
141 )
142 )
143 ))
144 ))
145 }
146
147 private ResultActions openViewTaskPage(taskId) throws Exception {
148 return mockMvc.perform(get('/task/{taskId}', taskId))
149 }
150}
@WithUsersDetails annotation to a feature method or to a specification class. Also, if you don’t remember how you can configure the authenticated user, you should take a look at the lesson: Writing Security Tests for Spring MVC Controllers
.
Let’s move on and find out how we can structure our specification classes when we are writing integration tests for controller methods that render data.
Structuring Our Specification Classes
We can structure our specification classes by following these rules:
- If the tested controller method renders all data found from a database table, we should write multiple specification classes because otherwise we cannot ensure that the system under specification is working as expected when the database table is empty and when it contains data.
- If the tested controller method renders data that fulfills the given search criteria, we should write only one specification class. We don’t need multiple specification classes because we can construct our test data and select the used search criteria in a way that allows us to test all required scenarios by using multiple when and then blocks.
- We should write one feature method per user role. This helps us to minimize the number of specification classes, and write integration tests which have a clean and logical structure. Also, if we follow this rule, the created HTML documentation looks logical as well.
We can now write integration tests for controllers methods that render data. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us four things:
- We can add the
@WithUsersDetailsannotation to a feature method or to a specification class. - If the tested controller method renders all data found from a database table, we should write multiple specification classes.
- If the tested controller method renders data that fulfills the given search criteria, we should write only one specification class.
- We should write one feature method per user role.