Writing Integration Tests for a Spring MVC REST API - Reading Data
After we have finished this lesson, we:
- Can write integration tests for controller methods which read data from the database and return the found data as JSON.
- 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
- You can write integration tests for controller methods that render data
Introduction to System Under Specification
We have to specify the expected behavior of the controller method that processes GET requests send to the path: ‘/api/task/{taskId}’. The name of this method is findById(), and it simply returns the information of the task that is found from the database.
The source code of the system under specification looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.web.bind.annotation.PathVariable;
3import org.springframework.web.bind.annotation.RequestMapping;
4import org.springframework.web.bind.annotation.RequestMethod;
5import org.springframework.web.bind.annotation.RestController;
6
7@RestController
8@RequestMapping("/api/task")
9public class TaskCrudController {
10
11 private final TaskCrudService crudService;
12
13 @Autowired
14 TaskCrudController(TaskCrudService crudService) {
15 this.crudService = crudService;
16 }
17
18 @RequestMapping(value = "{taskId}", method = RequestMethod.GET)
19 public TaskDTO findById(@PathVariable("taskId") Long taskId) {
20 return crudService.findById(taskId);
21 }
22}
If a closed task is found from the database and this task has one tag, the findById() method returns the following JSON document back to the client:
1{
2 "assignee": {
3 "name":"Anne Assignee",
4 "userId":44
5 },
6 "closer": {
7 "name":"Chris Closer",
8 "userId":931
9 },
10 "creationTime":"2017-07-30T11:41:28+03:00[Europe/Helsinki]",
11 "creator":{
12 "name":"John Doe",
13 "userId":99
14 },
15 "description":"description",
16 "id":1,
17 "modificationTime":"2017-07-31T11:41:28+03:00[Europe/Helsinki]",
18 "modifier":{
19 "name":"Jane Doe",
20 "userId":33
21 },
22 "resolution":"DONE",
23 "status":"CLOSED",
24 "tags":[
25 {
26 "id":33,
27 "name":"testing"
28 }
29 ],
30 "title":"title"
31}
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 FindTaskSpec 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 FindTaskSpec 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 create a private method that gets the task id as a method parameter and sends a GET request to the path: ‘/api/task/{taskId}’. This method returns a ResultActions object, and its goal is to help 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 FindTaskSpec 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 findTask(id) {
45 return mockMvc.perform(get('/api/task/{taskId}', id))
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 get the information of a task. 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: ‘/api/task/1’ and store the returnedResultActionsobject in theresponsevariable. - Ensure that the tested controller method returns the HTTP status code 401.
- Verify that the tested controller method returns an empty response.
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.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
18import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
20
21@ContextConfiguration(classes = [IntegrationTestContext.class])
22@WebAppConfiguration
23@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
24 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
25)
26@DatabaseSetup([
27 '/com/testwithspring/master/users.xml',
28 '/com/testwithspring/master/tasks.xml'
29])
30@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
31@Category(IntegrationTest.class)
32@ActiveProfiles(Profiles.INTEGRATION_TEST)
33class FindTaskSpec extends Specification {
34
35 @Autowired
36 WebApplicationContext webAppContext
37
38 def mockMvc
39
40 def setup() {
41 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
42 .apply(springSecurity())
43 .build()
44 }
45
46 def 'Find task as an anonymous user'() {
47
48 def response
49
50 when: 'An anonymous user tries to find the task'
51 response = findTask(Tasks.WriteExampleApp.ID)
52
53 then: 'Should return the HTTP status code unauthorized'
54 response.andExpect(status().isUnauthorized())
55
56 and: 'Should return an empty response'
57 response.andExpect(content().string(''))
58 }
59
60 private ResultActions findTask(id) {
61 return mockMvc.perform(get('/api/task/{taskId}', id))
62 }
63}
Fifth, we have to write a feature method which specifies the behavior of the system under specification when a registered user gets the information of a task. 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: ‘/api/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 returns the found task as JSON.
- Verify that the tested controller method returns the information of the found task.
- Ensure that the tested controller method returns the information of one tag.
- Verify that the tested controller method returns the information of the found 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.http.MediaType
8import org.springframework.security.test.context.support.WithUserDetails
9import org.springframework.test.context.ActiveProfiles
10import org.springframework.test.context.ContextConfiguration
11import org.springframework.test.context.TestExecutionListeners
12import org.springframework.test.context.web.WebAppConfiguration
13import org.springframework.test.web.servlet.ResultActions
14import org.springframework.test.web.servlet.setup.MockMvcBuilders
15import org.springframework.web.context.WebApplicationContext
16import spock.lang.Specification
17
18import static org.hamcrest.Matchers.hasSize
19import static org.hamcrest.Matchers.is
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.content
23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
24import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
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 FindTaskSpec 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 'Find task as an anonymous user'() {
52
53 def response
54
55 when: 'An anonymous user tries to find the task'
56 response = findTask(Tasks.WriteExampleApp.ID)
57
58 then: 'Should return the HTTP status code unauthorized'
59 response.andExpect(status().isUnauthorized())
60
61 and: 'Should return an empty response'
62 response.andExpect(content().string(''))
63 }
64
65 @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
66 def 'Find task as a registered user'() {
67
68 def response
69
70 when: 'The requested task is found'
71 response = findTask(Tasks.WriteExampleApp.ID)
72
73 then: 'Should return the HTTP status code OK'
74 response.andExpect(status().isOk())
75
76 and: 'Should return the found task as JSON'
77 response.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
78
79 and: 'Should return the information of the found task'
80 response.andExpect(jsonPath('$.assignee.userId',
81 is(Tasks.WriteExampleApp.Assignee.ID.intValue()))
82 )
83 .andExpect(jsonPath('$.assignee.name',
84 is(Tasks.WriteExampleApp.Assignee.NAME))
85 )
86 .andExpect(jsonPath('$.closer.userId',
87 is(Tasks.WriteExampleApp.Closer.ID.intValue()))
88 )
89 .andExpect(jsonPath('$.closer.name',
90 is(Tasks.WriteExampleApp.Closer.NAME))
91 )
92 .andExpect(jsonPath('$.creationTime',
93 is(Tasks.WriteExampleApp.CREATION_TIME_STRING))
94 )
95 .andExpect(jsonPath('$.creator.userId',
96 is(Tasks.WriteExampleApp.Creator.ID.intValue()))
97 )
98 .andExpect(jsonPath('$.creator.name',
99 is(Tasks.WriteExampleApp.Creator.NAME))
100 )
101 .andExpect(jsonPath('$.id',
102 is(Tasks.WriteExampleApp.ID.intValue()))
103 )
104 .andExpect(jsonPath('$.modificationTime',
105 is(Tasks.WriteExampleApp.MODIFICATION_TIME_STRING))
106 )
107 .andExpect(jsonPath('$.modifier.userId',
108 is(Tasks.WriteExampleApp.Modifier.ID.intValue()))
109 )
110 .andExpect(jsonPath('$.modifier.name',
111 is(Tasks.WriteExampleApp.Modifier.NAME))
112 )
113 .andExpect(jsonPath('$.title',
114 is(Tasks.WriteExampleApp.TITLE))
115 )
116 .andExpect(jsonPath('$.description',
117 is(Tasks.WriteExampleApp.DESCRIPTION))
118 )
119 .andExpect(jsonPath('$.status',
120 is(Tasks.WriteExampleApp.STATUS.toString()))
121 )
122 .andExpect(jsonPath('$.resolution',
123 is(Tasks.WriteExampleApp.RESOLUTION.toString()))
124 )
125
126 and: 'Should return task that has one tag'
127 response .andExpect(jsonPath('$.tags', hasSize(1)))
128
129 and: 'Should return the information of the correct tag'
130 response.andExpect(jsonPath('$.tags[0].id',
131 is(Tasks.WriteExampleApp.Tags.Example.ID.intValue()))
132 )
133 .andExpect(jsonPath('$.tags[0].name',
134 is(Tasks.WriteExampleApp.Tags.Example.NAME))
135 )
136 }
137
138 private ResultActions findTask(id) {
139 return mockMvc.perform(get('/api/task/{taskId}', id))
140 }
141}
@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 controllers methods that read data from the database and return the found data as JSON.
Structuring Our Specification Classes
We can structure our specification classes by following these rules:
- If the tested controller method returns 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 returns 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 find data from the database and return the found data as JSON. 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 returns all data found from a database table, we should write multiple specification classes.
- If the tested controller method returns data that fulfills the given search criteria, we should write only one specification class.
- We should write one feature method per user role.