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.

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}
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.

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}
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/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 Users class contains the information of the users found from our database.
  • The Tasks class 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:

  1. Add a new feature method to our specification class.
  2. Define a variable that contains the response (a ResultActions object) returned by the tested controller method.
  3. Send a GET request to the path: ‘/api/task/1’ and store the returned ResultActions object in the response variable.
  4. Ensure that the tested controller method returns the HTTP status code 401.
  5. 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:

  1. Add a new feature method to our specification class.
  2. Configure the authenticated user. We can do this by annotating our feature method with the @WithUserDetails annotation.
  3. Define a variable that contains the response (a ResultActions object) returned by the tested controller method.
  4. Send a GET request to the path: ‘/api/task/1’ and store the returned ResultActions object in the response variable.
  5. Verify that the tested controller method returns the HTTP status code 200.
  6. Ensure that the tested controller method returns the found task as JSON.
  7. Verify that the tested controller method returns the information of the found task.
  8. Ensure that the tested controller method returns the information of one tag.
  9. 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}
By the way, we can add the @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 @WithUsersDetails annotation 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.

Get the source code from Github