Writing Integration Tests for a Spring MVC REST API - Writing Data
After we have finished this lesson, we:
- Can write integration tests for a REST API that writes information to the database.
- 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 process form submissions
- You can write integration tests for a REST API that reads information from the database
Introduction to System Under Specification
We have to specify the behavior of the controller method that processes POST requests send to the path: ‘/api/task’. The name of this method is create(), and it is implemented by following these steps:
- Read the information of the created task from the request body and validate the information.
- If validation errors are found, return the HTTP status code 400 and describe the validation errors in the response body.
- Create a new task and return the information of the created task. Remember to return the HTTP status code 201.
The source code of the system under specification looks as follows:
1import org.springframework.beans.factory.annotation.Autowired;
2import org.springframework.http.HttpStatus;
3import org.springframework.security.core.annotation.AuthenticationPrincipal;
4import org.springframework.web.bind.annotation.RequestBody;
5import org.springframework.web.bind.annotation.RequestMapping;
6import org.springframework.web.bind.annotation.RequestMethod;
7import org.springframework.web.bind.annotation.ResponseStatus;
8import org.springframework.web.bind.annotation.RestController;
9
10import javax.validation.Valid;
11
12@RestController
13@RequestMapping("/api/task")
14public class TaskCrudController {
15
16 private final TaskCrudService crudService;
17
18 @Autowired
19 TaskCrudController(TaskCrudService crudService) {
20 this.crudService = crudService;
21 }
22
23 @RequestMapping(method = RequestMethod.POST)
24 @ResponseStatus(HttpStatus.CREATED)
25 public TaskDTO create(@Valid @RequestBody TaskFormDTO task,
26 @AuthenticationPrincipal LoggedInUser loggedInUser) {
27 return crudService.create(task, loggedInUser);
28 }
29}
The TaskFormDTO class contains the information 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 this information. Its source code 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 //Getters and setters are omitted
17}
The validation errors are processed by the TaskErrorHandler class. Its implementation doesn’t belong to the scope of this course, but it is useful to know what kind of a JSON document is returned to the client if validation fails. For example, if the client tries to create a new task that has no title, the following JSON document is returned to the client:
1{
2 "fieldErrors":[
3 {
4 "field":"title",
5 "errorCode":"NotBlank"
6 }
7 ]
8}
After the create() method has created a new task, it returns the information of the created task back to the client. For example, the returned JSON document could look as follows:
1{
2 "creationTime":"2017-07-30T11:41:28+03:00[Europe/Helsinki]",
3 "creator":{
4 "name":"John Doe",
5 "userId":99
6 },
7 "description":"description",
8 "id":1,
9 "modificationTime":"2017-07-31T11:41:28+03:00[Europe/Helsinki]",
10 "modifier":{
11 "name":"John Doe",
12 "userId":99
13 },
14 "status":"OPEN",
15 "tags":[],
16 "title":"title"
17}
Next, we will find out how we can specify the expected behavior of the system under specification when validation fails.
Specifying the Expected Behavior
Scenario 1: Validation Fails
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 CreateInvalidTaskSpec 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/no-tasks-and-tags.xml'
25])
26@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
27@Category(IntegrationTest.class)
28@ActiveProfiles(Profiles.INTEGRATION_TEST)
29class CreateInvalidTaskSpec 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>
Because the system under specification saves a new task to the database, we want to ensure that the tasks table has zero rows. Our DbUnit data set (no-tasks-and-tags.xml) looks as follows:
1<dataset>
2 <tasks/>
3 <tags/>
4</dataset>
Third, we have to write a private method that tries to create a task that has empty title and description. 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 com.testwithspring.master.IntegrationTest
6import com.testwithspring.master.IntegrationTestContext
7import com.testwithspring.master.config.Profiles
8import com.testwithspring.master.task.TaskFormDTO
9import org.junit.experimental.categories.Category
10import org.springframework.beans.factory.annotation.Autowired
11import org.springframework.http.MediaType
12import org.springframework.test.context.ActiveProfiles
13import org.springframework.test.context.ContextConfiguration
14import org.springframework.test.context.TestExecutionListeners
15import org.springframework.test.context.web.WebAppConfiguration
16import org.springframework.test.web.servlet.ResultActions
17import org.springframework.test.web.servlet.setup.MockMvcBuilders
18import org.springframework.web.context.WebApplicationContext
19import spock.lang.Specification
20
21import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
22import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
23import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
24
25@ContextConfiguration(classes = [IntegrationTestContext.class])
26@WebAppConfiguration
27@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
28 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
29)
30@DatabaseSetup([
31 '/com/testwithspring/master/users.xml',
32 '/com/testwithspring/master/no-tasks-and-tags.xml'
33])
34@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
35@Category(IntegrationTest.class)
36@ActiveProfiles(Profiles.INTEGRATION_TEST)
37class CreateInvalidTaskSpec extends Specification {
38
39 @Autowired
40 WebApplicationContext webAppContext
41
42 def mockMvc
43
44 def setup() {
45 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
46 .apply(springSecurity())
47 .build()
48 }
49
50 private ResultActions createEmptyTask() {
51 def input = new TaskFormDTO(title: '', description: '')
52 return mockMvc.perform(post('/api/task')
53 .contentType(MediaType.APPLICATION_JSON_UTF8)
54 .content(ObjectIntoJsonConverter.convertObjectIntoJsonBytes(input))
55 .with(csrf())
56 )
57 }
58}
ObjectIntoJsonConverter class is a utility class that converts objects into JSON byte arrays that can be send to the tested REST API.
Fourth, we have to write a feature method which specifies the behavior of the system under specification when an anonymous user tries to create a task that has empty title and description. We can write this feature method by following these steps:
- Add a new feature method to our specification class.
- Ensure that the system under specification doesn’t save a new task to the database.
- Define a variable that contains the response (a
ResultActionsobject) returned by the tested controller method. - Create a new task that has empty title and description, and store the returned
ResultActionsobject in theresponsevariable. - Verify that the tested controller method returns the HTTP status code 401.
- Ensure 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.annotation.ExpectedDatabase
5import com.github.springtestdbunit.assertion.DatabaseAssertionMode
6import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
7import org.junit.experimental.categories.Category
8import org.springframework.beans.factory.annotation.Autowired
9import org.springframework.http.MediaType
10import org.springframework.test.context.ActiveProfiles
11import org.springframework.test.context.ContextConfiguration
12import org.springframework.test.context.TestExecutionListeners
13import org.springframework.test.context.web.WebAppConfiguration
14import org.springframework.test.web.servlet.ResultActions
15import org.springframework.test.web.servlet.setup.MockMvcBuilders
16import org.springframework.web.context.WebApplicationContext
17import spock.lang.Specification
18
19import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
20import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
21import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
22import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
24
25@ContextConfiguration(classes = [IntegrationTestContext.class])
26@WebAppConfiguration
27@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
28 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
29)
30@DatabaseSetup([
31 '/com/testwithspring/master/users.xml',
32 '/com/testwithspring/master/no-tasks-and-tags.xml'
33])
34@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
35@Category(IntegrationTest.class)
36@ActiveProfiles(Profiles.INTEGRATION_TEST)
37class CreateInvalidTaskSpec extends Specification {
38
39 @Autowired
40 WebApplicationContext webAppContext
41
42 def mockMvc
43
44 def setup() {
45 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
46 .apply(springSecurity())
47 .build()
48 }
49
50 @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
51 assertionMode = DatabaseAssertionMode.NON_STRICT
52 )
53 def 'Create empty task as an anonymous user'() {
54
55 def response
56
57 when: 'An anonymous user tries to create an empty task'
58 response = createEmptyTask()
59
60 then: 'Should return the HTTP code status code unauthorized'
61 response.andExpect(status().isUnauthorized())
62
63 and: 'Should return an empty response'
64 response.andExpect(content().string(''))
65 }
66
67 private ResultActions createEmptyTask() {
68 def input = new TaskFormDTO(title: '', description: '')
69 return mockMvc.perform(post('/api/task')
70 .contentType(MediaType.APPLICATION_JSON_UTF8)
71 .content(ObjectIntoJsonConverter.convertObjectIntoJsonBytes(input))
72 .with(csrf())
73 )
74 }
75}
Fifth, we have to write a feature method which specifies the behavior of the system under specification when a registered user tries to create a task that has empty title and description. We can write this feature method by following these steps:
- Add a new feature method to our specification class and configure the authenticated user.
- Ensure that the system under specification doesn’t save a new task to the database.
- Define a variable that contains the response (a
ResultActionsobject) returned by the tested controller method. - Create a new task that has empty title and description, and store the returned
ResultActionsobject in theresponsevariable. - Ensure that the tested controller method returns the HTTP status code 400.
- Verify that the tested controller method returns validation errors as JSON.
- Ensure that the tested controller method returns one validation error.
- Verify that the tested controller method returns validation error about an empty title.
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.annotation.ExpectedDatabase
5import com.github.springtestdbunit.assertion.DatabaseAssertionMode
6import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
7import org.junit.experimental.categories.Category
8import org.springframework.beans.factory.annotation.Autowired
9import org.springframework.http.MediaType
10import org.springframework.security.test.context.support.WithUserDetails
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.hamcrest.Matchers.hasSize
21import static org.hamcrest.Matchers.is
22import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
23import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
24import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
25import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
26import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
27import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
28
29@ContextConfiguration(classes = [IntegrationTestContext.class])
30@WebAppConfiguration
31@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
32 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
33)
34@DatabaseSetup([
35 '/com/testwithspring/master/users.xml',
36 '/com/testwithspring/master/no-tasks-and-tags.xml'
37])
38@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
39@Category(IntegrationTest.class)
40@ActiveProfiles(Profiles.INTEGRATION_TEST)
41class CreateInvalidTaskSpec extends Specification {
42
43 @Autowired
44 WebApplicationContext webAppContext
45
46 def mockMvc
47
48 def setup() {
49 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
50 .apply(springSecurity())
51 .build()
52 }
53
54 @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
55 assertionMode = DatabaseAssertionMode.NON_STRICT
56 )
57 def 'Create empty task as an anonymous user'() {
58
59 def response
60
61 when: 'An anonymous user tries to create an empty task'
62 response = createEmptyTask()
63
64 then: 'Should return the HTTP code status code unauthorized'
65 response.andExpect(status().isUnauthorized())
66
67 and: 'Should return an empty response'
68 response.andExpect(content().string(''))
69 }
70
71 @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
72 @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
73 assertionMode = DatabaseAssertionMode.NON_STRICT
74 )
75 def 'Create empty task as a registered user'() {
76
77 def response
78
79 when: 'A registered user tries to create an empty task'
80 response = createEmptyTask()
81
82 then: 'Should return the HTTP status code bad request'
83 response.andExpect(status().isBadRequest())
84
85 and: 'Should return validation errors as JSON'
86 response.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
87
88 and: 'Should return one validation error'
89 response.andExpect(jsonPath('$.fieldErrors', hasSize(1)))
90
91 and: 'Should return a validation error about an empty title'
92 response.andExpect(jsonPath('$.fieldErrors[0].field', is('title')))
93 .andExpect(jsonPath('$.fieldErrors[0].errorCode',
94 is('NotBlank')
95 ))
96 }
97
98 private ResultActions createEmptyTask() {
99 def input = new TaskFormDTO(title: '', description: '')
100 return mockMvc.perform(post('/api/task')
101 .contentType(MediaType.APPLICATION_JSON_UTF8)
102 .content(ObjectIntoJsonConverter.convertObjectIntoJsonBytes(input))
103 .with(csrf())
104 )
105 }
106}
Let’s move on and find out how we can specify the expected behavior of the system under specification when validation is successful.
Scenario 2: Validation Is Successful
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. Again, 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 CreateValidTaskSpec 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. Our feature methods will use the same data sets that we used in the previous example.
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/no-tasks-and-tags.xml'
25])
26@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
27@Category(IntegrationTest.class)
28@ActiveProfiles(Profiles.INTEGRATION_TEST)
29class CreateValidTaskSpec 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}
Third, we have to inject the IdColumnReset bean into our specification class and ensure that the id of the created task is always 1.
After we have done this, 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/no-tasks-and-tags.xml'
25])
26@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
27@Category(IntegrationTest.class)
28@ActiveProfiles(Profiles.INTEGRATION_TEST)
29class CreateValidTaskSpec extends Specification {
30
31 @Autowired
32 WebApplicationContext webAppContext
33
34 @Autowired
35 IdColumnReset idColumnReset
36
37 def mockMvc
38
39 def setup() {
40 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
41 .apply(springSecurity())
42 .build()
43
44 idColumnReset.resetIdColumns('tasks')
45 }
46}
IdColumnReset bean is, you should take a look at the lesson: Resetting Generated Id Column Values Before Every Test Method
.
Fourth, we have to write a private method that creates a new task by using valid information and returns a ResultActions object.
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.http.MediaType
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.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
18import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
19import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
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/no-tasks-and-tags.xml'
29])
30@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
31@Category(IntegrationTest.class)
32@ActiveProfiles(Profiles.INTEGRATION_TEST)
33class CreateValidTaskSpec extends Specification {
34
35 @Autowired
36 WebApplicationContext webAppContext
37
38 @Autowired
39 IdColumnReset idColumnReset
40
41 def mockMvc
42
43 def setup() {
44 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
45 .apply(springSecurity())
46 .build()
47
48 idColumnReset.resetIdColumns('tasks')
49 }
50
51 private ResultActions createTask() {
52 def input = new TaskFormDTO(title: Tasks.WriteExampleApp.TITLE,
53 description: Tasks.WriteExampleApp.DESCRIPTION
54 )
55
56 return mockMvc.perform(post('/api/task')
57 .contentType(MediaType.APPLICATION_JSON_UTF8)
58 .content(ObjectIntoJsonConverter.convertObjectIntoJsonBytes(input))
59 .with(csrf())
60 )
61 }
62}
Fifth, we have to write a feature method which specifies the behavior of the system under specification when an anonymous user tries to create a new task by using valid information. We can write this feature method by following these steps:
- Add a new feature method to our specification class.
- Ensure that the system under specification doesn’t save a new task to the database.
- Define a variable that contains the response (a
ResultActionsobject) returned by the tested controller method. - Create a new task and store the returned
ResultActionsobject in theresponsevariable. - Verify that the tested controller method returns the HTTP status code 401.
- Ensure 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.annotation.ExpectedDatabase
5import com.github.springtestdbunit.assertion.DatabaseAssertionMode
6import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
7import org.junit.experimental.categories.Category
8import org.springframework.beans.factory.annotation.Autowired
9import org.springframework.http.MediaType
10import org.springframework.test.context.ActiveProfiles
11import org.springframework.test.context.ContextConfiguration
12import org.springframework.test.context.TestExecutionListeners
13import org.springframework.test.context.web.WebAppConfiguration
14import org.springframework.test.web.servlet.ResultActions
15import org.springframework.test.web.servlet.setup.MockMvcBuilders
16import org.springframework.web.context.WebApplicationContext
17import spock.lang.Specification
18
19import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
20import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
21import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
22import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
24
25@ContextConfiguration(classes = [IntegrationTestContext.class])
26@WebAppConfiguration
27@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
28 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
29)
30@DatabaseSetup([
31 '/com/testwithspring/master/users.xml',
32 '/com/testwithspring/master/no-tasks-and-tags.xml'
33])
34@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
35@Category(IntegrationTest.class)
36@ActiveProfiles(Profiles.INTEGRATION_TEST)
37class CreateValidTaskSpec extends Specification {
38
39 @Autowired
40 WebApplicationContext webAppContext
41
42 @Autowired
43 IdColumnReset idColumnReset
44
45 def mockMvc
46
47 def setup() {
48 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
49 .apply(springSecurity())
50 .build()
51
52 idColumnReset.resetIdColumns('tasks')
53 }
54
55 @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
56 assertionMode = DatabaseAssertionMode.NON_STRICT
57 )
58 def 'Create task as an anonymous user'() {
59
60 def response
61
62 when: 'An anonymous user tries to create a task'
63 response = createTask()
64
65 then: 'Should return the HTTP status code unauthorized'
66 response.andExpect(status().isUnauthorized())
67
68 and: 'Should return an empty response'
69 response.andExpect(content().string(''))
70 }
71
72 private ResultActions createTask() {
73 def input = new TaskFormDTO(title: Tasks.WriteExampleApp.TITLE,
74 description: Tasks.WriteExampleApp.DESCRIPTION
75 )
76
77 return mockMvc.perform(post('/api/task')
78 .contentType(MediaType.APPLICATION_JSON_UTF8)
79 .content(ObjectIntoJsonConverter.convertObjectIntoJsonBytes(input))
80 .with(csrf())
81 )
82 }
83}
Sixth, we have to write a feature method which specifies the behavior of the system under specification when a registered user creates a new task by using valid information. We can write this feature method by following these steps:
- Add a new feature method to our specification class and configure the authenticated user.
- Ensure that the system under specification saves a new task to the database.
- Define a variable that contains the response (a
ResultActionsobject) returned by the tested controller method. - Create a new task and store the returned
ResultActionsobject in theresponsevariable. - Verify that the tested controller method returns the HTTP status code 201.
- Ensure that the tested controller method returns the created task as JSON.
- Verify that the tested controller method returns the information of the created task.
- Ensure that the tested controller method returns a task that has no tags.
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.annotation.ExpectedDatabase
5import com.github.springtestdbunit.assertion.DatabaseAssertionMode
6import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
7import org.junit.experimental.categories.Category
8import org.springframework.beans.factory.annotation.Autowired
9import org.springframework.http.MediaType
10import org.springframework.security.test.context.support.WithUserDetails
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.hamcrest.Matchers.hasSize
21import static org.hamcrest.Matchers.is
22import static org.hamcrest.Matchers.nullValue
23import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
24import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
25import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
26import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
27import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
28import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
29
30@ContextConfiguration(classes = [IntegrationTestContext.class])
31@WebAppConfiguration
32@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
33 mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
34)
35@DatabaseSetup([
36 '/com/testwithspring/master/users.xml',
37 '/com/testwithspring/master/no-tasks-and-tags.xml'
38])
39@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
40@Category(IntegrationTest.class)
41@ActiveProfiles(Profiles.INTEGRATION_TEST)
42class CreateValidTaskSpec extends Specification {
43
44 @Autowired
45 WebApplicationContext webAppContext
46
47 @Autowired
48 IdColumnReset idColumnReset
49
50 def mockMvc
51
52 def setup() {
53 mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
54 .apply(springSecurity())
55 .build()
56
57 idColumnReset.resetIdColumns('tasks')
58 }
59
60 @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
61 assertionMode = DatabaseAssertionMode.NON_STRICT
62 )
63 def 'Create task as an anonymous user'() {
64
65 def response
66
67 when: 'An anonymous user tries to create a task'
68 response = createTask()
69
70 then: 'Should return the HTTP status code unauthorized'
71 response.andExpect(status().isUnauthorized())
72
73 and: 'Should return an empty response'
74 response.andExpect(content().string(''))
75 }
76
77 @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
78 @ExpectedDatabase(value = 'create-task-as-user-expected.xml',
79 assertionMode = DatabaseAssertionMode.NON_STRICT
80 )
81 def 'Create task as a registered user'() {
82
83 def response
84
85 when: 'A registered user creates a task by using valid information'
86 response = createTask()
87
88 then: 'Should return the HTTP status code created'
89 response.andExpect(status().isCreated())
90
91 and: 'Should return the created task as json'
92 response.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8))
93
94 and: 'Should return the information of the created task'
95 response.andExpect(jsonPath('$.assignee', nullValue()))
96 .andExpect(jsonPath('$.closer', nullValue()))
97 .andExpect(jsonPath('$.creationTime',
98 is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))
99 )
100 .andExpect(jsonPath('$.creator.userId',
101 is(Users.JohnDoe.ID.intValue()))
102 )
103 .andExpect(jsonPath('$.creator.name',
104 is(Users.JohnDoe.NAME))
105 )
106 .andExpect(jsonPath('$.id', is(1)))
107 .andExpect(jsonPath('$.modificationTime',
108 is(ConstantDateTimeService.CURRENT_DATE_AND_TIME))
109 )
110 .andExpect(jsonPath('$.modifier.userId',
111 is(Users.JohnDoe.ID.intValue()))
112 )
113 .andExpect(jsonPath('$.modifier.name',
114 is(Users.JohnDoe.NAME))
115 )
116 .andExpect(jsonPath('$.title',
117 is(Tasks.WriteExampleApp.TITLE))
118 )
119 .andExpect(jsonPath('$.description',
120 is(Tasks.WriteExampleApp.DESCRIPTION))
121 )
122 .andExpect(jsonPath('$.status',
123 is(TaskStatus.OPEN.toString()))
124 )
125 .andExpect(jsonPath('$.resolution', nullValue()))
126
127 and: 'Should return a task that has no tags'
128 response.andExpect(jsonPath('$.tags', hasSize(0)))
129 }
130
131 private ResultActions createTask() {
132 def input = new TaskFormDTO(title: Tasks.WriteExampleApp.TITLE,
133 description: Tasks.WriteExampleApp.DESCRIPTION
134 )
135
136 return mockMvc.perform(post('/api/task')
137 .contentType(MediaType.APPLICATION_JSON_UTF8)
138 .content(ObjectIntoJsonConverter.convertObjectIntoJsonBytes(input))
139 .with(csrf())
140 )
141 }
142}
The DbUnit data set (create-task-as-user-expected.xml) which ensures that a new task was saved to the database looks as follows:
1<dataset>
2 <tasks id="1"
3 assignee_id="[null]"
4 closer_id="[null]"
5 creation_time="2016-12-03 21:14:28.0"
6 creator_id="1"
7 description="This example contains integration tests"
8 modification_time="2016-12-03 21:14:28.0"
9 modifier_id="1"
10 resolution="[null]"
11 title="Write example application"
12 status="OPEN"
13 version="0"/>
14 <tags/>
15</dataset>
Next, we will find out how we can structure our specification classes when we are writing integration tests for a REST API that writes information to the database.
Structuring Our Specification Classes
When we create our specification classes, we must remember that we cannot write a feature method which specifies the behavior of the system under specification when validation fails and when validation is successful. The reason for this is that we have to add the @ExpectedDatabase annotation to our feature method.
That’s why we should structure our specification classes by following these rules:
- We should create one specification class which contains the feature methods that specify the behavior of the system under specification when validation fails.
- We should create one specification class which contains the feature methods that specify the behavior of the system under specification when validation is successful.
- 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 a REST API that writes information to the database. Let’s summarize what we learned from this lesson.
Summary
This lesson has taught us three things:
- We should create one specification class that specifies the behavior of the system under specification when validation fails.
- We should create one specification class that specifies the behavior of the system under specification when validation is successful.
- We should write one feature method per user role.