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.

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:

  1. Read the information of the created task from the request body and validate the information.
  2. If validation errors are found, return the HTTP status code 400 and describe the validation errors in the response body.
  3. 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}
Also, the security requirements described in the lesson: Writing Security Tests for Spring MVC Controllers are valid for this lesson as well. If you haven’t finished that lesson, I recommend that you do it now.

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}
If you don’t know how you can configure the system under specification, you should take a look at the lesson: Configuring the System Under Specification .

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}
The 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:

  1. Add a new feature method to our specification class.
  2. Ensure that the system under specification doesn’t save a new task to the database.
  3. Define a variable that contains the response (a ResultActions object) returned by the tested controller method.
  4. Create a new task that has empty title and description, and store the returned ResultActions object in the response variable.
  5. Verify that the tested controller method returns the HTTP status code 401.
  6. 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:

  1. Add a new feature method to our specification class and configure the authenticated user.
  2. Ensure that the system under specification doesn’t save a new task to the database.
  3. Define a variable that contains the response (a ResultActions object) returned by the tested controller method.
  4. Create a new task that has empty title and description, and store the returned ResultActions object in the response variable.
  5. Ensure that the tested controller method returns the HTTP status code 400.
  6. Verify that the tested controller method returns validation errors as JSON.
  7. Ensure that the tested controller method returns one validation error.
  8. 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}
If you don’t know what the 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:

  1. Add a new feature method to our specification class.
  2. Ensure that the system under specification doesn’t save a new task to the database.
  3. Define a variable that contains the response (a ResultActions object) returned by the tested controller method.
  4. Create a new task and store the returned ResultActions object in the response variable.
  5. Verify that the tested controller method returns the HTTP status code 401.
  6. 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:

  1. Add a new feature method to our specification class and configure the authenticated user.
  2. Ensure that the system under specification saves a new task to the database.
  3. Define a variable that contains the response (a ResultActions object) returned by the tested controller method.
  4. Create a new task and store the returned ResultActions object in the response variable.
  5. Verify that the tested controller method returns the HTTP status code 201.
  6. Ensure that the tested controller method returns the created task as JSON.
  7. Verify that the tested controller method returns the information of the created task.
  8. 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.

Get the source code from Github