Writing Integration Tests for "Normal" Spring MVC Controllers - Rendering Data

After we have finished this lesson, we

  • Know how we can write integration tests for normal Spring MVC controllers that render data.
  • Understand how we should structure our specification classes when we specify the behavior of the system under specification.

Introduction to System Under Specification

We have to specify the behavior of the controller method that processes GET requests send to the path: ‘/task/{taskId}’. The name of this method is showTask(), and it is implemented by following these steps:

  1. Find the task from the database.
  2. Put the found task to a model attribute called: task.
  3. Return the name of the view (’task/view’) that renders the information of the found task.

The source code of the system under specification looks as follows:

 1import com.testwithspring.intermediate.task.TaskCrudService;
 2import com.testwithspring.intermediate.task.TaskDTO;
 3import org.springframework.beans.factory.annotation.Autowired;
 4import org.springframework.context.MessageSource;
 5import org.springframework.stereotype.Controller;
 6import org.springframework.ui.Model;
 7import org.springframework.web.bind.annotation.PathVariable;
 8import org.springframework.web.bind.annotation.RequestMapping;
 9import org.springframework.web.bind.annotation.RequestMethod;
10
11@Controller
12public class TaskCrudController {
13
14    private final TaskCrudService crudService;
15    private final MessageSource messageSource;
16
17    @Autowired
18    TaskCrudController(TaskCrudService crudService,
19                       MessageSource messageSource) {
20        this.crudService = crudService;
21        this.messageSource = messageSource;
22    }
23
24    @RequestMapping(value = "/task/{taskId}", method = RequestMethod.GET)
25    public String showTask(@PathVariable("taskId") Long taskId, Model model) {
26        TaskDTO task = crudService.findById(taskId);
27        model.addAttribute("task", task);
28        return "task/view";
29    }
30}

The TaskDTO class is a data transfer object that contains the information of a single task. Its source code looks as follows:

 1import java.time.ZonedDateTime;
 2import java.util.ArrayList;
 3import java.util.List;
 4
 5public class TaskDTO {
 6
 7    private PersonDTO assignee;
 8    private PersonDTO closer;
 9    private ZonedDateTime creationTime;
10    private PersonDTO creator;
11    private String description;
12    private Long id;
13    private ZonedDateTime modificationTime;
14    private PersonDTO modifier;
15    private TaskResolution resolution;
16    private TaskStatus status;
17    private List<TagDTO> tags = new ArrayList<>();
18    private String title;
19
20    //Getters and setters are omitted
21}

The PersonDTO class contains the information of a single person. Its source code looks as follows:

1public class PersonDTO {
2
3    private String name;
4    private Long userId;
5
6    //Getters and setters are omitted
7}

The TagDTO class contains the information of a single tag. Its source code looks as follows:

1public class TagDTO {
2
3    private Long id;
4    private String name;
5
6    //Getters and setters are omitted
7}
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 ShowTaskSpec extends Specification {
25
26    @Autowired
27    WebApplicationContext webAppContext
28
29    def mockMvc
30
31    def setup() {
32        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
33                .apply(springSecurity())
34                .build()
35    }
36}
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 ShowTaskSpec extends Specification {
30
31    @Autowired
32    WebApplicationContext webAppContext
33
34    def mockMvc
35
36    def setup() {
37        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
38                .apply(springSecurity())
39                .build()
40    }
41}

The DbUnit data set that inserts the required test data into the user_accounts table (users.xml) looks as follows:

 1<dataset>
 2    <user_accounts id="1"
 3                   creation_time="2017-02-11 11:41:28"
 4                   email_address="john.doe@gmail.com"
 5                   is_enabled="true"
 6                   modification_time="2017-02-11 11:41:28"
 7                   name="John Doe"
 8                   password="user"
 9                   role="ROLE_USER"
10                   version="0"/>
11    <user_accounts id="2"
12                   creation_time="2017-02-11 11:41:28"
13                   email_address="anne.admin@gmail.com"
14                   is_enabled="true"
15                   modification_time="2017-02-11 11:41:28"
16                   name="Anne Admin"
17                   password="admin"
18                   role="ROLE_ADMIN"
19                   version="0"/>
20</dataset>

The DbUnit data set that inserts the required test data into the tasks table (tasks.xml) looks as follows:

 1<dataset>
 2    <tasks id="1"
 3           assignee_id="1"
 4           closer_id="1"
 5           creation_time="2016-12-3 11:41:28"
 6           creator_id="1"
 7           description="This example contains integration tests"
 8           modification_time="2016-12-3 11:41:28"
 9           modifier_id="1"
10           resolution="DONE"
11           status="CLOSED"
12           title="Write example application"
13           version="0"/>
14    <tasks id="2"
15           assignee_id="[null]"
16           closer_id="[null]"
17           creation_time="2016-12-4 11:41:28"
18           creator_id="1"
19           description="This lesson talks about integration testing"
20           modification_time="2016-12-4 11:41:28"
21           modifier_id="1"
22           resolution="[null]"
23           status="OPEN"
24           title="Write lesson"
25           version="0"/>
26
27    <tags id="1"
28          creation_time="2016-12-3 11:41:28"
29          modification_time="2016-12-3 11:41:28"
30          name="example"
31          version="0"/>
32    <tags id="2"
33          creation_time="2016-12-3 11:41:28"
34          modification_time="2016-12-3 11:41:28"
35          name="lesson"
36          version="0"/>
37
38    <tasks_tags task_id="1" tag_id="1"/>
39    <tasks_tags task_id="2" tag_id="2"/>
40</dataset>

During this lesson we will use two constant classes:

  • The 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 write a private method that gets the task id as a method parameter and sends a GET request to the path: ‘/task/{taskId}’. This method returns a ResultActions object, and it helps us to reduce the duplicate code which can be found from specification classes that use the Spring MVC Test framework.

After we have added this method to our specification class, its source code looks as follows:

 1import com.github.springtestdbunit.DbUnitTestExecutionListener
 2import com.github.springtestdbunit.annotation.DatabaseSetup
 3import com.github.springtestdbunit.annotation.DbUnitConfiguration
 4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
 5import org.junit.experimental.categories.Category
 6import org.springframework.beans.factory.annotation.Autowired
 7import org.springframework.test.context.ActiveProfiles
 8import org.springframework.test.context.ContextConfiguration
 9import org.springframework.test.context.TestExecutionListeners
10import org.springframework.test.context.web.WebAppConfiguration
11import org.springframework.test.web.servlet.ResultActions
12import org.springframework.test.web.servlet.setup.MockMvcBuilders
13import org.springframework.web.context.WebApplicationContext
14import spock.lang.Specification
15
16import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
17import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
18
19@ContextConfiguration(classes = [IntegrationTestContext.class])
20@WebAppConfiguration
21@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
22        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
23)
24@DatabaseSetup([
25        '/com/testwithspring/master/users.xml',
26        '/com/testwithspring/master/tasks.xml'
27])
28@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
29@Category(IntegrationTest.class)
30@ActiveProfiles(Profiles.INTEGRATION_TEST)
31class ShowTaskSpec extends Specification {
32
33    @Autowired
34    WebApplicationContext webAppContext
35
36    def mockMvc
37
38    def setup() {
39        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
40                .apply(springSecurity())
41                .build()
42    }
43
44    private ResultActions openViewTaskPage(taskId) throws Exception {
45        return  mockMvc.perform(get('/task/{taskId}', taskId))
46    }
47}

Fourth, we have to write a feature method which specifies the behavior of the system under specification when an anonymous user tries to open the view task page. We can write this feature method by following these steps:

  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: ‘/task/1’ and store the returned ResultActions object in the response variable.
  4. Verify that the tested controller method returns the HTTP status code 302.
  5. Ensure that the tested controller method redirects the user to the login page.

After we have written this feature method, the source code of our specification class looks as follows:

 1import com.github.springtestdbunit.DbUnitTestExecutionListener
 2import com.github.springtestdbunit.annotation.DatabaseSetup
 3import com.github.springtestdbunit.annotation.DbUnitConfiguration
 4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
 5import com.testwithspring.master.IntegrationTest
 6import com.testwithspring.master.IntegrationTestContext
 7import com.testwithspring.master.Tasks
 8import com.testwithspring.master.config.Profiles
 9import org.junit.experimental.categories.Category
10import org.springframework.beans.factory.annotation.Autowired
11import org.springframework.test.context.ActiveProfiles
12import org.springframework.test.context.ContextConfiguration
13import org.springframework.test.context.TestExecutionListeners
14import org.springframework.test.context.web.WebAppConfiguration
15import org.springframework.test.web.servlet.ResultActions
16import org.springframework.test.web.servlet.setup.MockMvcBuilders
17import org.springframework.web.context.WebApplicationContext
18import spock.lang.Specification
19
20import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
21import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
22import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
23
24@ContextConfiguration(classes = [IntegrationTestContext.class])
25@WebAppConfiguration
26@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
27        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
28)
29@DatabaseSetup([
30        '/com/testwithspring/master/users.xml',
31        '/com/testwithspring/master/tasks.xml'
32])
33@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
34@Category(IntegrationTest.class)
35@ActiveProfiles(Profiles.INTEGRATION_TEST)
36class ShowTaskSpec extends Specification {
37
38    @Autowired
39    WebApplicationContext webAppContext
40
41    def mockMvc
42
43    def setup() {
44        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
45                .apply(springSecurity())
46                .build()
47    }
48
49    def 'Open view task task page as an anonymous user'() {
50
51        def response
52
53        when: 'An anonymous user tries to open the view task page'
54        response = openViewTaskPage(Tasks.WriteExampleApp.ID)
55
56        then: 'Should return the HTTP status code found'
57        response.andExpect(status().isFound())
58
59        and: 'Should redirect the user to the login page'
60        response.andExpect(redirectedUrl('http://localhost/user/login'))
61    }
62
63    private ResultActions openViewTaskPage(taskId) throws Exception {
64        return  mockMvc.perform(get('/task/{taskId}', taskId))
65    }
66}

Fifth, we have to write a feature method which specifies the behavior of the system under specification when a registered user opens the view task page. We can write this feature method by following these steps:

  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: ‘/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 renders the view task view.
  7. Verify that the tested controller method forwards the user to view task page.
  8. Ensure that the tested controller method displays the information of the found task.
  9. Verify that the tested controller method displays the information of one tag.
  10. Ensure that the tested controller method displays the information of the correct tag.

After we have written this feature method, the source code of our specification class looks as follows:

  1import com.github.springtestdbunit.DbUnitTestExecutionListener
  2import com.github.springtestdbunit.annotation.DatabaseSetup
  3import com.github.springtestdbunit.annotation.DbUnitConfiguration
  4import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
  5import org.junit.experimental.categories.Category
  6import org.springframework.beans.factory.annotation.Autowired
  7import org.springframework.security.test.context.support.WithUserDetails
  8import org.springframework.test.context.ActiveProfiles
  9import org.springframework.test.context.ContextConfiguration
 10import org.springframework.test.context.TestExecutionListeners
 11import org.springframework.test.context.web.WebAppConfiguration
 12import org.springframework.test.web.servlet.ResultActions
 13import org.springframework.test.web.servlet.setup.MockMvcBuilders
 14import org.springframework.web.context.WebApplicationContext
 15import spock.lang.Specification
 16
 17import static org.hamcrest.Matchers.allOf
 18import static org.hamcrest.Matchers.contains
 19import static org.hamcrest.Matchers.hasProperty
 20import static org.hamcrest.Matchers.hasSize
 21import static org.hamcrest.Matchers.is
 22import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
 23import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
 24import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
 25
 26@ContextConfiguration(classes = [IntegrationTestContext.class])
 27@WebAppConfiguration
 28@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
 29        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
 30)
 31@DatabaseSetup([
 32        '/com/testwithspring/master/users.xml',
 33        '/com/testwithspring/master/tasks.xml'
 34])
 35@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
 36@Category(IntegrationTest.class)
 37@ActiveProfiles(Profiles.INTEGRATION_TEST)
 38class ShowTaskSpec extends Specification {
 39
 40    @Autowired
 41    WebApplicationContext webAppContext
 42
 43    def mockMvc
 44
 45    def setup() {
 46        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
 47                .apply(springSecurity())
 48                .build()
 49    }
 50
 51    def 'Open view task task page as an anonymous user'() {
 52
 53        def response
 54
 55        when: 'An anonymous user tries to open the view task page'
 56        response = openViewTaskPage(Tasks.WriteExampleApp.ID)
 57
 58        then: 'Should return the HTTP status code found'
 59        response.andExpect(status().isFound())
 60
 61        and: 'Should redirect the user to the login page'
 62        response.andExpect(redirectedUrl('http://localhost/user/login'))
 63    }
 64
 65    @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
 66    def 'Open view task page as a registered user'() {
 67
 68        def response
 69
 70        when: 'The requested task is found'
 71        response = openViewTaskPage(Tasks.WriteExampleApp.ID)
 72
 73        then: 'Should return the HTTP status code OK'
 74        response.andExpect(status().isOk())
 75
 76        and: 'Should render the view task view'
 77        response.andExpect(view().name('task/view'))
 78
 79        and: 'Should forward the user to the view task page'
 80        response.andExpect(forwardedUrl('/WEB-INF/jsp/task/view.jsp'))
 81
 82        and: 'Should display the information of the found task'
 83        response.andExpect(model().attribute('task', allOf(
 84                hasProperty('assignee', allOf(
 85                        hasProperty('name',
 86                                is(Tasks.WriteExampleApp.Assignee.NAME)
 87                        ),
 88                        hasProperty('userId',
 89                                is(Tasks.WriteExampleApp.Assignee.ID)
 90                        )
 91                )),
 92                hasProperty('closer', allOf(
 93                        hasProperty('name',
 94                                is(Tasks.WriteExampleApp.Closer.NAME)
 95                        ),
 96                        hasProperty('userId',
 97                                is(Tasks.WriteExampleApp.Closer.ID)
 98                        )
 99                )),
100                hasProperty('creationTime', is(Tasks.WriteExampleApp.CREATION_TIME)),
101                hasProperty('creator', allOf(
102                        hasProperty('name',
103                                is(Tasks.WriteExampleApp.Creator.NAME)
104                        ),
105                        hasProperty('userId',
106                                is(Tasks.WriteExampleApp.Creator.ID)
107                        )
108                )),
109                hasProperty('id', is(Tasks.WriteExampleApp.ID)),
110                hasProperty('modificationTime', is(Tasks.WriteExampleApp.MODIFICATION_TIME)),
111                hasProperty('modifier', allOf(
112                        hasProperty('name',
113                                is(Tasks.WriteExampleApp.Modifier.NAME)
114                        ),
115                        hasProperty('userId',
116                                is(Tasks.WriteExampleApp.Modifier.ID)
117                        )
118                )),
119                hasProperty('title', is(Tasks.WriteExampleApp.TITLE)),
120                hasProperty('description', is(Tasks.WriteExampleApp.DESCRIPTION)),
121                hasProperty('status', is(Tasks.WriteExampleApp.STATUS)),
122                hasProperty('resolution', is(Tasks.WriteExampleApp.RESOLUTION))
123        )))
124
125        and: 'Should display one tag of the found task'
126        response .andExpect(model().attribute('task',
127                hasProperty('tags', hasSize(1))
128        ))
129
130        and: 'Should display the information of the found tag'
131        response.andExpect(model().attribute('task',
132                hasProperty('tags', contains(
133                        allOf(
134                                hasProperty(
135                                        'id',
136                                        is(Tasks.WriteExampleApp.Tags.Example.ID)
137                                ),
138                                hasProperty(
139                                        'name',
140                                        is(Tasks.WriteExampleApp.Tags.Example.NAME)
141                                )
142                        )
143                ))
144        ))
145    }
146
147    private ResultActions openViewTaskPage(taskId) throws Exception {
148        return  mockMvc.perform(get('/task/{taskId}', taskId))
149    }
150}
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 controller methods that render data.

Structuring Our Specification Classes

We can structure our specification classes by following these rules:

  • If the tested controller method renders all data found from a database table, we should write multiple specification classes because otherwise we cannot ensure that the system under specification is working as expected when the database table is empty and when it contains data.
  • If the tested controller method renders data that fulfills the given search criteria, we should write only one specification class. We don’t need multiple specification classes because we can construct our test data and select the used search criteria in a way that allows us to test all required scenarios by using multiple when and then blocks.
  • We should write one feature method per user role. This helps us to minimize the number of specification classes, and write integration tests which have a clean and logical structure. Also, if we follow this rule, the created HTML documentation looks logical as well.

We can now write integration tests for controllers methods that render data. Let’s summarize what we learned from this lesson.

Summary

This lesson has taught us four things:

  • We can add the @WithUsersDetails annotation to a feature method or to a specification class.
  • If the tested controller method renders all data found from a database table, we should write multiple specification classes.
  • If the tested controller method renders data that fulfills the given search criteria, we should write only one specification class.
  • We should write one feature method per user role.

Get the source code from Github