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.
This lesson assumes that:
- You are familiar with DbUnit
- You can configure your integration tests which use JUnit 4.
- You can write integration tests for Spring web applications by using JUnit 4.
- You are familiar with Spock Framework
- You can configure the system under specification when you write integration tests with Spock Framework
- You can use DbUnit with Spock Framework
- You can write integration tests for controllers methods that render data
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:
- If the submitted form has validation errors, return the name of the form view (’task/create’).
- Save the created task to the database by invoking the
create()method of theTaskCrudServiceinterface. - 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.
- 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}
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}
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
Usersclass contains the information of the users found from our database. - The
Tasksclass 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:
- Add a new feature method to our specification class.
- Ensure that the system under specification doesn’t save a new task to the database.
- Define a variable that contains the response (a
ResultActionsobject) returned by tested controller method. - Submit an empty create task form and store the returned
ResultActionsobject in theresponsevariable. - Verify that the tested controller method returns the HTTP status code 302.
- 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:
- Add a new feature method to our specification class and configure the authenticated user.
- Ensure that the system under specification doesn’t save a new task to the database.
- Define a variable that contains the response (a
ResultActionsobject) returned by tested controller method. - Submit an empty create task form and store the returned
ResultActionsobject in theresponsevariable. - Ensure that the tested controller method returns the HTTP status code 200.
- Verify that the tested controller method displays the create task view.
- Ensure that the tested controller method forwards the user to the create task page.
- Verify that the tested controller method display a validation error about an empty title.
- Ensure that the tested controller method displays an empty create task form.
- 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}
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:
- Add a new feature method to our specification class.
- Ensure that the system under specification doesn’t save a new task to the database.
- Define a variable that contains the response (a
ResultActionsobject) returned by tested controller method. - Submit the create task form and store the returned
ResultActionsobject in theresponsevariable. - Verify that the tested controller method returns the HTTP status code 302.
- 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:
- Add a new feature method to our specification class and configure the authenticated user.
- Ensure that the system under specification saves a new task to the database.
- Define a variable that contains the response (a
ResultActionsobject) returned by tested controller method. - Submit the create task form and store the returned
ResultActionsobject in theresponsevariable. - Ensure that the tested controller method returns the HTTP status code 302.
- Verify that the tested controller method redirects the user to the view task view.
- Ensure that the tested controller method redirects the user to the view task page.
- 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.