Writing Integration Tests for "Normal" Spring MVC Controllers - Forms

After we have finished this lesson, we

  • Know how we can write integration tests for controller methods that process form submissions.
  • 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 POST requests send to the path: ‘/task/create’. The name of this method is processCreateTaskForm(), and it is implemented by following these steps:

  1. If the submitted form has validation errors, return the name of the form view (’task/create’).
  2. Save the created task to the database by invoking the create() method of the TaskCrudService interface.
  3. Create a feedback message which states that a new task was created and ensure that this message is shown when the next view is rendered.
  4. Redirect the request to the view that renders the information of the created task.

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

 1import org.springframework.beans.factory.annotation.Autowired;
 2import org.springframework.context.MessageSource;
 3import org.springframework.security.core.annotation.AuthenticationPrincipal;
 4import org.springframework.stereotype.Controller;
 5import org.springframework.validation.BindingResult;
 6import org.springframework.web.bind.annotation.ModelAttribute;
 7import org.springframework.web.bind.annotation.RequestMapping;
 8import org.springframework.web.bind.annotation.RequestMethod;
 9import org.springframework.web.servlet.mvc.support.RedirectAttributes;
10
11import javax.validation.Valid;
12import java.util.Locale;
13
14@Controller
15public class TaskCrudController {
16
17    private final TaskCrudService crudService;
18    private final MessageSource messageSource;
19
20    @Autowired
21    TaskCrudController(TaskCrudService crudService, MessageSource messageSource) {
22        this.crudService = crudService;
23        this.messageSource = messageSource;
24    }
25
26    @RequestMapping(value = "/task/create", method = RequestMethod.POST)
27    public String processCreateTaskForm(@Valid @ModelAttribute("task") TaskFormDTO formObject,
28                                        BindingResult bindingResult,
29                                        RedirectAttributes redirectAttributes,
30                                        Locale currentLocale,
31                                        @AuthenticationPrincipal LoggedInUser loggedInUser) {
32        if (bindingResult.hasErrors()) {
33            return "task/create";
34        }
35
36        TaskDTO created = crudService.create(formObject, loggedInUser);
37        addFeedbackMessage(redirectAttributes,
38                "feedback.message.task.created",
39                currentLocale,
40                created.getTitle()
41        );
42        redirectAttributes.addAttribute("taskId", created.getId());
43
44        return "redirect:/task/{taskId}";
45    }
46
47    private void addFeedbackMessage(RedirectAttributes attributes,
48                                    String messageCode,
49                                    Locale currentLocale,
50                                    Object... messageParameters) {
51        String localizedFeedbackMessage = messageSource.getMessage(messageCode,
52                messageParameters,
53                currentLocale
54        );
55        attributes.addFlashAttribute("feedbackMessage",
56                localizedFeedbackMessage
57        );
58    }
59}

The TaskFormDTO class contains the information of the form object 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 the form object. The source code of the TaskFormDTO class looks as follows:

 1public class TaskFormDTO {
 2
 3    private Long id;
 4
 5    @Size(max = 500)
 6    private String description;
 7
 8    @NotBlank
 9    @Size(max = 100)
10    private String title;
11
12    //Getters and setters are omitted
13}
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.springframework.beans.factory.annotation.Autowired
 5import org.springframework.test.context.ActiveProfiles
 6import org.springframework.test.context.ContextConfiguration
 7import org.springframework.test.context.TestExecutionListeners
 8import org.springframework.test.context.web.WebAppConfiguration
 9import org.springframework.test.web.servlet.setup.MockMvcBuilders
10import org.springframework.web.context.WebApplicationContext
11import spock.lang.Specification
12
13import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
14
15@ContextConfiguration(classes = [IntegrationTestContext.class])
16@WebAppConfiguration
17@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
18        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
19)
20@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
21@Category(IntegrationTest.class)
22@ActiveProfiles(Profiles.INTEGRATION_TEST)
23class ProcessInvalidCreateTaskFormSpec extends Specification {
24
25    @Autowired
26    WebApplicationContext webAppContext
27
28    def mockMvc
29
30    def setup() {
31        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
32                .apply(springSecurity())
33                .build()
34    }
35}
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 users and tasks into our database.

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 ProcessInvalidCreateTaskFormSpec 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 our database has no tasks. Our DbUnit data set (no-tasks-and-tags.xml) looks as follows:

1<dataset>
2    <tasks/>
3    <tags/>
4</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 submits an empty create task form. 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.request.SecurityMockMvcRequestPostProcessors.csrf
17import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
18import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
19
20@ContextConfiguration(classes = [IntegrationTestContext.class])
21@WebAppConfiguration
22@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
23        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
24)
25@DatabaseSetup([
26        '/com/testwithspring/master/users.xml',
27        '/com/testwithspring/master/no-tasks-and-tags.xml'
28])
29@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
30@Category(IntegrationTest.class)
31@ActiveProfiles(Profiles.INTEGRATION_TEST)
32class ProcessInvalidCreateTaskFormSpec extends Specification {
33
34    @Autowired
35    WebApplicationContext webAppContext
36
37    def mockMvc
38
39    def setup() {
40        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
41                .apply(springSecurity())
42                .build()
43    }
44
45    private ResultActions submitEmptyCreateTaskForm() throws Exception {
46        return  mockMvc.perform(post('/task/create')
47                .param('description', '')
48                .param('title', '')
49                .with(csrf())
50        )
51    }
52}

Fourth, we have to write a feature method which specifies the behavior of the system under specification when an anonymous user submits an empty create task form. 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 tested controller method.
  4. Submit an empty create task form and store the returned ResultActions object in the response variable.
  5. Verify that the tested controller method returns the HTTP status code 302.
  6. 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.annotation.ExpectedDatabase
 5import com.github.springtestdbunit.assertion.DatabaseAssertionMode
 6import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
 7import com.testwithspring.master.*
 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.request.SecurityMockMvcRequestPostProcessors.csrf
21import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
22import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl
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/no-tasks-and-tags.xml'
34])
35@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
36@Category(IntegrationTest.class)
37@ActiveProfiles(Profiles.INTEGRATION_TEST)
38class ProcessInvalidCreateTaskFormSpec 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    @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
52            assertionMode = DatabaseAssertionMode.NON_STRICT
53    )
54    def 'Submit an invalid create task form as an anonymous user'() {
55
56        def response
57
58        when: 'An anonymous user submits an empty create task form'
59        response = submitEmptyCreateTaskForm()
60
61        then: 'Should return the HTTP status code found'
62        response.andExpect(status().isFound())
63
64        and: 'Should redirect the user to the login page'
65        response.andExpect(redirectedUrl('http://localhost/user/login'))
66    }
67
68    private ResultActions submitEmptyCreateTaskForm() throws Exception {
69        return  mockMvc.perform(post('/task/create')
70                .param('description', '')
71                .param('title', '')
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 submits an empty create task form. 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 tested controller method.
  4. Submit an empty create task form and store the returned ResultActions object in the response variable.
  5. Ensure that the tested controller method returns the HTTP status code 200.
  6. Verify that the tested controller method displays the create task view.
  7. Ensure that the tested controller method forwards the user to the create task page.
  8. Verify that the tested controller method display a validation error about an empty title.
  9. Ensure that the tested controller method displays an empty create task form.
  10. Verify that the tested controller method doesn’t set the id of the form object.

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.security.test.context.support.WithUserDetails
 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.hamcrest.Matchers.allOf
 20import static org.hamcrest.Matchers.hasProperty
 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.forwardedUrl
 27import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model
 28import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl
 29import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
 30import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view
 31
 32@ContextConfiguration(classes = [IntegrationTestContext.class])
 33@WebAppConfiguration
 34@TestExecutionListeners(value = DbUnitTestExecutionListener.class,
 35        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
 36)
 37@DatabaseSetup([
 38        '/com/testwithspring/master/users.xml',
 39        '/com/testwithspring/master/no-tasks-and-tags.xml'
 40])
 41@DbUnitConfiguration(dataSetLoader = ReplacementDataSetLoader.class)
 42@Category(IntegrationTest.class)
 43@ActiveProfiles(Profiles.INTEGRATION_TEST)
 44class ProcessInvalidCreateTaskFormSpec extends Specification {
 45
 46    @Autowired
 47    WebApplicationContext webAppContext
 48
 49    def mockMvc
 50
 51    def setup() {
 52        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
 53                .apply(springSecurity())
 54                .build()
 55    }
 56
 57    @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
 58            assertionMode = DatabaseAssertionMode.NON_STRICT
 59    )
 60    def 'Submit an invalid create task form as an anonymous user'() {
 61
 62        def response
 63
 64        when: 'An anonymous user submits an empty create task form'
 65        response = submitEmptyCreateTaskForm()
 66
 67        then: 'Should return the HTTP status code found'
 68        response.andExpect(status().isFound())
 69
 70        and: 'Should redirect the user to the login page'
 71        response.andExpect(redirectedUrl('http://localhost/user/login'))
 72    }
 73
 74    @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
 75    @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
 76            assertionMode = DatabaseAssertionMode.NON_STRICT
 77    )
 78    def 'Submit an invalid create task form as a registered user'() {
 79
 80        def response
 81
 82        when: 'A registered user submits an empty create task form'
 83        response = submitEmptyCreateTaskForm()
 84
 85        then: 'Should return the HTTP status code OK'
 86        response.andExpect(status().isOk())
 87
 88        and: 'Should render the create task view'
 89        response.andExpect(view().name('task/create'))
 90
 91        and: 'Should forward the user to the create task page'
 92        response.andExpect(forwardedUrl('/WEB-INF/jsp/task/create.jsp'))
 93
 94        and: 'Should show a validation error about an empty title'
 95        response.andExpect(model().attributeHasFieldErrorCode('task',
 96                'title',
 97                is('NotBlank')
 98        ))
 99
100        and: 'Should display an empty create task form'
101        response.andExpect(model().attribute('task', allOf(
102                hasProperty('description', is('')),
103                hasProperty('title', is(''))
104        )))
105
106        and: 'Should not modify the hidden id parameter'
107        response.andExpect(model().attribute('task', allOf(
108                hasProperty('id', nullValue())
109        )))
110    }
111
112    private ResultActions submitEmptyCreateTaskForm() throws Exception {
113        return  mockMvc.perform(post('/task/create')
114                .param('description', '')
115                .param('title', '')
116                .with(csrf())
117        )
118    }
119}

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 ProcessValidCreateTaskFormSpec 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 ProcessValidCreateTaskFormSpec 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 ProcessValidCreateTaskFormSpec extends Specification {
30
31    @Autowired
32    IdColumnReset idColumnReset
33
34    @Autowired
35    WebApplicationContext webAppContext
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 create a private method that submits the create task form 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 com.testwithspring.master.config.Profiles
 6import org.junit.experimental.categories.Category
 7import org.springframework.beans.factory.annotation.Autowired
 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 ProcessValidCreateTaskFormSpec extends Specification {
34
35    @Autowired
36    IdColumnReset idColumnReset
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        idColumnReset.resetIdColumns('tasks')
49    }
50
51    private ResultActions submitCreateTaskForm() throws Exception {
52        return  mockMvc.perform(post('/task/create')
53                .param('description', Tasks.WriteExampleApp.DESCRIPTION)
54                .param('title', Tasks.WriteExampleApp.TITLE)
55                .with(csrf())
56        )
57    }
58}

Fifth, we have to write a feature method which specifies the behavior of the system under specification when an anonymous user submits the create task form 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 tested controller method.
  4. Submit the create task form and store the returned ResultActions object in the response variable.
  5. Verify that the tested controller method returns the HTTP status code 302.
  6. 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.annotation.ExpectedDatabase
 5import com.github.springtestdbunit.assertion.DatabaseAssertionMode
 6import com.github.springtestdbunit.dataset.ReplacementDataSetLoader
 7import com.testwithspring.master.IdColumnReset
 8import com.testwithspring.master.IntegrationTest
 9import com.testwithspring.master.IntegrationTestContext
10import com.testwithspring.master.Tasks
11import com.testwithspring.master.config.Profiles
12import org.junit.experimental.categories.Category
13import org.springframework.beans.factory.annotation.Autowired
14import org.springframework.test.context.ActiveProfiles
15import org.springframework.test.context.ContextConfiguration
16import org.springframework.test.context.TestExecutionListeners
17import org.springframework.test.context.web.WebAppConfiguration
18import org.springframework.test.web.servlet.ResultActions
19import org.springframework.test.web.servlet.setup.MockMvcBuilders
20import org.springframework.web.context.WebApplicationContext
21import spock.lang.Specification
22
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.redirectedUrl
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 ProcessValidCreateTaskFormSpec extends Specification {
42
43    @Autowired
44    IdColumnReset idColumnReset
45
46    @Autowired
47    WebApplicationContext webAppContext
48
49    def mockMvc
50
51    def setup() {
52        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
53                .apply(springSecurity())
54                .build()
55
56        idColumnReset.resetIdColumns('tasks')
57    }
58
59    @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
60            assertionMode = DatabaseAssertionMode.NON_STRICT
61    )
62    def 'Submit a valid create task form as an anonymous user'() {
63
64        def response
65
66        when: 'An anonymous user submits the create task form'
67        response = submitCreateTaskForm()
68
69        then: 'Should return the HTTP status code found'
70        response.andExpect(status().isFound())
71
72        and: 'Should redirect the user to the login page'
73        response.andExpect(redirectedUrl('http://localhost/user/login'))
74    }
75
76    private ResultActions submitCreateTaskForm() throws Exception {
77        return  mockMvc.perform(post('/task/create')
78                .param('description', Tasks.WriteExampleApp.DESCRIPTION)
79                .param('title', Tasks.WriteExampleApp.TITLE)
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 submits the create task form 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 tested controller method.
  4. Submit the create task form and store the returned ResultActions object in the response variable.
  5. Ensure that the tested controller method returns the HTTP status code 302.
  6. Verify that the tested controller method redirects the user to the view task view.
  7. Ensure that the tested controller method redirects the user to the view task page.
  8. Verify that the tested controller method displays the correct feedback message to the user.

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.security.test.context.support.WithUserDetails
 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.hamcrest.Matchers.is
 20import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
 21import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
 22import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
 23import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.flash
 24import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model
 25import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl
 26import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
 27import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view
 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 ProcessValidCreateTaskFormSpec extends Specification {
 42
 43    @Autowired
 44    IdColumnReset idColumnReset
 45
 46    @Autowired
 47    WebApplicationContext webAppContext
 48
 49    def mockMvc
 50
 51    def setup() {
 52        mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext)
 53                .apply(springSecurity())
 54                .build()
 55
 56        idColumnReset.resetIdColumns('tasks')
 57    }
 58
 59    @ExpectedDatabase(value = '/com/testwithspring/master/no-tasks-and-tags.xml',
 60            assertionMode = DatabaseAssertionMode.NON_STRICT
 61    )
 62    def 'Submit a valid create task form as an anonymous user'() {
 63
 64        def response
 65
 66        when: 'An anonymous user submits the create task form'
 67        response = submitCreateTaskForm()
 68
 69        then: 'Should return the HTTP status code found'
 70        response.andExpect(status().isFound())
 71
 72        and: 'Should redirect the user to the login page'
 73        response.andExpect(redirectedUrl('http://localhost/user/login'))
 74    }
 75
 76    @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS)
 77    @ExpectedDatabase(value = 'create-task-as-user-expected.xml',
 78            assertionMode = DatabaseAssertionMode.NON_STRICT
 79    )
 80    def 'Submit a valid create task form as a registered user'() {
 81
 82        def response
 83
 84        when: 'A registered user submits the create task form'
 85        response = submitCreateTaskForm()
 86
 87        then: 'Should return the HTTP status code found'
 88        response.andExpect(status().isFound())
 89
 90        and: 'Should redirect the user to the view task view'
 91        response.andExpect(view().name('redirect:/task/{taskId}'))
 92                .andExpect(model().attribute('taskId', is('1')))
 93
 94        and: 'Should redirect the user to view task page'
 95        response.andExpect(redirectedUrl('/task/1'))
 96
 97        and: 'Should display the feedback message to the user'
 98        response.andExpect(flash().attribute('feedbackMessage',
 99                'A new task was created successfully.'
100        ))
101    }
102
103    private ResultActions submitCreateTaskForm() throws Exception {
104        return  mockMvc.perform(post('/task/create')
105                .param('description', Tasks.WriteExampleApp.DESCRIPTION)
106                .param('title', Tasks.WriteExampleApp.TITLE)
107                .with(csrf())
108        )
109    }
110}

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 controller methods that process form submissions.

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 can 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 controllers methods that process form submissions. 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