This is the third part of my spring-test-mvc tutorial and it describes how we can write integration tests for controller methods that are processing form submissions.
During this tutorial we will continue writing integration tests for a simple todo application. This tutorial concentrates on two functions that are used to create new todo entries and update the information of existing todo entries.
Getting the Required Dependencies
Our tests use Jackson to convert form objects in to strings that are send in the body of the performed POST request. Thus, we have to declare the Jackson dependencies in our pom.xml file. We can do this by following these steps:
- Declare the jackson-core-asl (version 1.9.9) dependency in our pom.xml file.
- Declare the jackson-mapper-asl (version 1.9.9) dependency in our pom.xml file.
We can do this by adding the following snippet to our pom.xml file:
<dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-core-asl</artifactId> <version>1.9.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>1.9.9</version> <scope>test</scope> </dependency>
The Anatomy of Our Todo Application
Before we can write integration tests for our todo application, we need to take a quick look at its anatomy. This section describes the used form object, the used service interface and the controller implementation.
The Form Object
The form object of our todo application is rather simple. It has only a few fields, and its only methods are simple getters and setters. It also declares the following validation rules:
- The title of an todo entry cannot be empty.
- The maximum length of the todo entry’s title is 100 characters.
- The maximum length of the todo entry’s description is 500 characters.
The source code of the TodoDTO class looks as follows:
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.NotEmpty; public class TodoDTO { private Long id; @Length(max = 500) private String description; @NotEmpty @Length(max = 100) private String title; public TodoDTO() { } //Getters and setters }
The Service Interface
If we want to understand the implementation of our controller methods, we have to understand the contract between our controller class and the service layer. This contract is described by the TodoService interface that declares two new methods:
- The Todo add(TodoDTO added) method adds a new todo entry and returns a the added entry.
- The Todo update(TodoDTO updated) method updates the information of a single todo entry and returns the updated entry. If no todo entry is found, this method throws TodoNotFoundException.
The source code of the TodoService interface looks as follows:
public interface TodoService { public Todo add(TodoDTO added); public Todo update(TodoDTO updated) throws TodoNotFoundException; }
The Controller
The TodoController class has four methods that processes requests that are related to adding and updating todo entries. These methods are described in the following:
- The showAddTodoForm() method shows the page that contains the add todo entry form.
- The add() methods processes the form submissions of the add todo form.
- The showUpdateTodoForm() method shows the page that contains the update todo entry form.
- The update() method processes the form submissions of the update todo form.
The source code of the TodoController class looks as follows:
import org.springframework.context.MessageSource; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; import javax.annotation.Resource; import javax.validation.Valid; @Controller @SessionAttributes("todo") public class TodoController { @Resource private TodoService service; @Resource private MessageSource messageSource; @RequestMapping(value = "/todo/add", method = RequestMethod.GET) public String showAddTodoForm(Model model) { TodoDTO formObject = new TodoDTO(); model.addAttribute("todo", formObject); return "todo/add"; } @RequestMapping(value = "/todo/add", method = RequestMethod.POST) public String add(@Valid @ModelAttribute("todo") TodoDTO dto, BindingResult result, RedirectAttributes attributes) { if (result.hasErrors()) { return "todo/add"; } Todo added = service.add(dto); addFeedbackMessage(attributes, "feedback.message.todo.added", added.getTitle()); attributes.addAttribute("id", added.getId()); return createRedirectViewPath("/todo/{id}"); } @RequestMapping(value = "/todo/update/{id}", method = RequestMethod.GET) public String showUpdateTodoForm(@PathVariable("id") Long id, Model model) throws TodoNotFoundException { Todo updated = service.findById(id); TodoDTO formObject = constructFormObjectForUpdateForm(updated); model.addAttribute("todo", formObject); return "todo/update"; } @RequestMapping(value = "/todo/update", method = RequestMethod.POST) public String update(@Valid @ModelAttribute("todo") TodoDTO dto, BindingResult result, RedirectAttributes attributes) throws TodoNotFoundException { if (result.hasErrors()) { return "todo/update"; } Todo updated = service.update(dto); addFeedbackMessage(attributes, "feedback.message.todo.updated", updated.getTitle()); attributes.addAttribute("id", updated.getId()); return createRedirectViewPath("/todo/{id}"); } private TodoDTO constructFormObjectForUpdateForm(Todo updated) { TodoDTO dto = new TodoDTO(); dto.setId(updated.getId()); dto.setDescription(updated.getDescription()); dto.setTitle(updated.getTitle()); return dto; } private void addFeedbackMessage(RedirectAttributes attributes, String messageCode, Object... messageParameters) { String localizedFeedbackMessage = getMessage(messageCode, messageParameters); attributes.addFlashAttribute("feedbackMessage", localizedFeedbackMessage); } private String getMessage(String messageCode, Object... messageParameters) { Locale current = LocaleContextHolder.getLocale(); return messageSource.getMessage(messageCode, messageParameters, current); } private String createRedirectViewPath(String requestMapping) { StringBuilder redirectViewPath = new StringBuilder(); redirectViewPath.append("redirect:"); redirectViewPath.append(requestMapping); return redirectViewPath.toString(); } }
Writing Integration Tests for Forms
The section describes how we can write integration tests for the forms of our todo application. Let's move on and take a look at the common testing utilities which we use in our integration tests.
Common Testing Utilities
We use two testing utilities in our integration tests. These test utilities are:
- The TodoTestUtil class is used in both unit and integration tests of our todo application.
- The toDoData.xml is a DBUnit dataset that initializes the used database to a known state before our tests are run.
These utilities are described with more details in following.
The TodoTestUtil Class
The TodoTestUtil class has one static method which is used used in our integration tests. The createStringWithLength(int length) method is used to create new String objects.
The source code of the TodoTestUtil class looks as follows:
public class TodoTestUtil { public static String createStringWithLength(int length) { StringBuilder builder = new StringBuilder(); for (int index = 0; index < length; index++) { builder.append("a"); } return builder.toString(); } }
The DBUnit Dataset
Each integration test uses the same DBUnit dataset which is found from the toDoData.xml file. The content of this dataset looks as follows:
<dataset> <todos id="1" creation_time="2012-10-21 11:13:28" description="Lorem ipsum" modification_time="2012-10-21 11:13:28" title="Foo" version="0"/> <todos id="2" creation_time="2012-10-21 11:13:28" description="Lorem ipsum" modification_time="2012-10-21 11:13:28" title="Bar" version="0"/> </dataset>
Show Add Todo Form
The showAddTodoForm() method of the TodoController class is used to view the page that contains the add todo form. We can write an integration test for this method by following these steps:
- Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
- Perform a GET request to url '/todo/add'.
- Verify that the HTTP status code is 200.
- Verify that name of the rendered view is 'todo/add'.
- Verify that the request is forwarded to url '/WEB-INF/jsp/todo/add.jsp'.
- Verify that each field of our form object is empty.
The source code of our integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase("toDoData.xml") public void showAddTodoForm() throws Exception { mockMvc.perform(get("/todo/add")) .andExpect(status().isOk()) .andExpect(view().name("todo/add")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/add.jsp")) .andExpect(model().attribute("todo", hasProperty("id", nullValue()))) .andExpect(model().attribute("todo", hasProperty("description", isEmptyOrNullString()))) .andExpect(model().attribute("todo", hasProperty("title", isEmptyOrNullString()))); } }
Add Todo
The add() method of the TodoController class is responsible of processing the form submissions of the add todo form. We have to write three integration tests for this method. These tests are described in following:
- We have to write a test that ensures that the method is working correctly when an empty add todo form is submitted.
- We have to write a test that ensures that the method is working properly when the title and the description of the todo entry are too long, and the add todo form is submitted.
- We have to write a test that ensures that this method is working correctly when a new todo entry is added.
These tests are described with more details in following.
Submit an Empty Add Todo Form
We can write the first integration test by following these steps:
- Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
- Perform a POST request to url '/todo/add' by following these steps:
- Set the content type of the request to 'application/x-www-form-urlencoded'.
- Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
- Verify that the HTTP status code is 200.
- Verify that the name of the rendered view is '/todo/add'.
- Verify that the request is forwarded to url '/WEB-INF/jsp/todo/add.jsp'.
- Verify that there is a field error in the title field.
- Verify that our form object is empty.
The source code of our first integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase("toDoData.xml") public void addEmptyTodo() throws Exception { mockMvc.perform(post("/todo/add") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .sessionAttr("todo", new TodoDTO()) ) .andExpect(status().isOk()) .andExpect(view().name("todo/add")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/add.jsp")) .andExpect(model().attributeHasFieldErrors("todo", "title")) .andExpect(model().attribute("todo", hasProperty("id", nullValue()))) .andExpect(model().attribute("todo", hasProperty("description", isEmptyOrNullString()))) .andExpect(model().attribute("todo", hasProperty("title", isEmptyOrNullString()))); } }
Submit Add Todo Form with Validation Errors
We can write the second integration test by following these steps:
- Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
- Create title and description of the todo entry.
- Perform a POST request to url '/todo/add' by following these steps:
- Set the content type of the request to 'application/x-www-form-urlencoded'.
- Send the description and title of the todo entry as request parameters.
- Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
- Verify that the HTTP status code is 200.
- Verify that the name of the rendered view is '/todo/add'.
- Verify that the request is forwarded to url '/WEB-INF/jsp/todo/add.jsp'.
- Verify that there are a field errors in the title and description fields.
- Verify that our form object contains the correct values.
The source code of our second integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase("toDoData.xml") public void addTodoWhenTitleAndDescriptionAreTooLong() throws Exception { String title = TodoTestUtil.createStringWithLength(101); String description = TodoTestUtil.createStringWithLength(501); mockMvc.perform(post("/todo/add") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("description", description) .param("title", title) .sessionAttr("todo", new TodoDTO()) ) .andExpect(status().isOk()) .andExpect(view().name("todo/add")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/add.jsp")) .andExpect(model().attributeHasFieldErrors("todo", "title")) .andExpect(model().attributeHasFieldErrors("todo", "description")) .andExpect(model().attribute("todo", hasProperty("id", nullValue()))) .andExpect(model().attribute("todo", hasProperty("description", is(description)))) .andExpect(model().attribute("todo", hasProperty("title", is(title)))); } }
Submit Add Todo Form
We can write the third integration test by following these steps:
- Use the @ExpectedDatabase annotation to verify that a new todo entry is added to database.
- Perform a POST request to url '/todo/add' by following these steps:
- Set the content type of the request to 'application/x-www-form-urlencoded'.
- Send the description and title of the todo entry as request parameters.
- Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
- Verify that the HTTP status code is 200.
- Verify that the name of the rendered view is 'redirect:/todo/view/{id}'.
- Verify that the model has an attribute caled ‘id’ and that its value is 3.
- Verify that the correct feedback message is given as a flash attribute.
The source code of our third integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import com.github.springtestdbunit.assertion.DatabaseAssertionMode; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase(value="toDoData-add-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) public void addTodo() throws Exception { mockMvc.perform(post("/todo/add") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("description", "description") .param("title", "title") .sessionAttr("todo", new TodoDTO()) ) .andExpect(status().isOk()) .andExpect(view().name("redirect:/todo/view/{id}")) .andExpect(model().attribute("id", is("3"))) .andExpect(flash().attribute("feedbackMessage", is("Todo entry: title was added."))); } }
The name of the used DBUnit dataset is toDoData-add-expected.xml and its content is given in the following:
<dataset> <todos id="1" description="Lorem ipsum" title="Foo" version="0"/> <todos id="2" description="Lorem ipsum" title="Bar" version="0"/> <todos id="3" description="description" title="title" version="0"/> </dataset>
Show Update Todo Form
The showUpdateTodoForm() method of the TodoController class views the page that contains the update todo form. We have to write two integration tests for this method. These tests are described in the following:
- We have to write a test that ensures that this method is working correctly when the updated todo entry is found.
- We have to write a test that ensures that this method is working properly when the updated todo entry is not found.
These tests are described with more details in following.
Show Update Todo Form
We can write the first integration test by following these steps:
- Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
- Perform a GET request to url '/todo/update/1'.
- Verify that the HTTP status code is 200.
- Verify that the name of the rendered view is 'todo/update'.
- Verify that the request is forwarded to url '/WEB-INF/jsp/todo/update.jsp'.
- Verify that our form object contains the correct information.
The source code of our first integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase("toDoData.xml") public void showUpdateTodoForm() throws Exception { mockMvc.perform(get("/todo/update/{id}", 1L)) .andExpect(status().isOk()) .andExpect(view().name("todo/update")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/update.jsp")) .andExpect(model().attribute("todo", hasProperty("id", is(1L)))) .andExpect(model().attribute("todo", hasProperty("description", is("Lorem ipsum")))) .andExpect(model().attribute("todo", hasProperty("title", is("Foo")))); } }
Show Update Todo Form When the Todo Entry Is Not Found
We can write the second integration test by following these steps:
- Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
- Perform a GET to request to url '/todo/update/3'.
- Verify that the HTTP status code is 404.
- Verify that the name of the rendered view is 'error/404'.
- Verify that the request is forwarded to url '/WEB-INF/jsp/error/404.jsp'.
The source code of our second integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase("toDoData.xml") public void showUpdateTodoFormWhenTodoIsNotFound() throws Exception { mockMvc.perform(get("/todo/update/{id}", 3L)) .andExpect(status().isNotFound()) .andExpect(view().name("error/404")) .andExpect(forwardedUrl("/WEB-INF/jsp/error/404.jsp")); } }
Update Todo
The update() method of the TodoController class processes the form submissions of the update todo form. We have to write four integration tests for this method. These integration tests are described in the following:
- We have to write an integration test that ensures that the method is working properly when an empty update todo form is submitted.
- We have to write an integration test that ensures the method is working properly when the title and description of the todo entry are too long, and the update todo form is submitted.
- We have to write an integration test that ensures that the method is working correctly when the information of a todo entry is updated.
- We have to write an integration test that ensures that the method is working properly when the updated todo entry is not found.
These tests are described with more details in following.
Submit an Empty Update Todo Form
We can write the first integration test by following these steps:
- Use the @ExpectedDatabase annotation to verify that no changes are made to the database.
- Perform a POST request to url '/todo/update' by following these steps:
- Set the content type of the request to 'application/x-www-form-urlencoded'.
- Send the id of the todo entry as a request parameter
- Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
- Verify that the HTTP status code is 200.
- Verify that the name of the rendered view is '/todo/update'.
- Verify that the request is forwarded to url '/WEB-INF/jsp/todo/update.jsp'.
- Verify that there is a field error in the title field.
- Verify that our form object is empty.
The source code of our first integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase("toDoData.xml") public void updateEmptyTodo() throws Exception { mockMvc.perform(post("/todo/update") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("id", "1") .sessionAttr("todo", new TodoDTO()) ) .andExpect(status().isOk()) .andExpect(view().name("todo/update")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/update.jsp")) .andExpect(model().attributeHasFieldErrors("todo", "title")) .andExpect(model().attribute("todo", hasProperty("id", is(1L)))) .andExpect(model().attribute("todo", hasProperty("description", isEmptyOrNullString()))) .andExpect(model().attribute("todo", hasProperty("title", isEmptyOrNullString()))); } }
Submit Update Todo Form with Validation Errors
We can write the second integration test by following these steps:
- Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
- Create the title and the description of the todo entry.
- Perform a POST request to url '/todo/update' by following these steps:
- Set the content type of the request to 'application/x-www-form-urlencoded'.
- Send the description, id, and title of the todo entry as request parameters
- Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
- Verify that the HTTP status code is 200.
- Verify that the name of the rendered view is '/todo/update'.
- Verify that the request is forwarded to url '/WEB-INF/jsp/todo/update.jsp'.
- Verify that there are field errors in the title and description fields.
- Verify that our form object contains the correct values.
The source code of our second integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.hamcrest.Matchers.*; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase("toDoData.xml") public void updateTodoWhenTitleAndDescriptionAreTooLong() throws Exception { String title = TodoTestUtil.createStringWithLength(101); String description = TodoTestUtil.createStringWithLength(501); mockMvc.perform(post("/todo/update") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("description", description) .param("id", "1") .param("title", title) .sessionAttr("todo", new TodoDTO()) ) .andExpect(status().isOk()) .andExpect(view().name("todo/update")) .andExpect(forwardedUrl("/WEB-INF/jsp/todo/update.jsp")) .andExpect(model().attributeHasFieldErrors("todo", "title")) .andExpect(model().attributeHasFieldErrors("todo", "description")) .andExpect(model().attribute("todo", hasProperty("id", is(1L)))) .andExpect(model().attribute("todo", hasProperty("description", is(description)))) .andExpect(model().attribute("todo", hasProperty("title", is(title)))); } }
Submit Update Todo Form
We can write the third integration test by following these steps:
- Use the @ExpectedDatabase annotation to verify that the information of the todo entry is updated.
- Perform a POST request to url '/todo/update' by following these steps:
- Set the content type of the request to 'application/x-www-form-urlencoded'.
- Send the description, id, and title of the todo entry as request parameters
- Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
- Verify that the HTTP status code is 200.
- Verify that the name of the rendered view is 'redirect:/todo/view/{id}'.
- Verify that the model has an attribute ‘id’ and that its value is 1.
- Verify that the correct feedback message is given as a flash attribute.
The source code of our third integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import com.github.springtestdbunit.assertion.DatabaseAssertionMode; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.hamcrest.Matchers.is; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase(value="toDoData-update-expected.xml", assertionMode = DatabaseAssertionMode.NON_STRICT) public void updateTodo() throws Exception { mockMvc.perform(post("/todo/update") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("description", "description") .param("id", "1") .param("title", "title") .sessionAttr("todo", new TodoDTO()) ) .andExpect(status().isOk()) .andExpect(view().name("redirect:/todo/view/{id}")) .andExpect(model().attribute("id", is("1"))) .andExpect(flash().attribute("feedbackMessage", is("Todo entry: title was updated."))); } }
The name of the used DBUnit dataset is toDoData-update-expected.xml and its content is given in the following:
<dataset> <todos id="1" description="description" title="title" version="1"/> <todos id="2" description="Lorem ipsum" title="Bar" version="0"/> </dataset>
Submit Update Todo Form When the Todo Entry Is Not Found
We can write the fourth integration test by following these steps:
- Use the @ExpectedDatabase annotation to verify that no changes is made to the database.
- Perform a POST request to url '/todo/update' by following these steps:
- Set the content type of the request to 'application/x-www-form-urlencoded'.
- Send the description, id, and title of the todo entry as request parameters
- Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
- Verify that the HTTP status code is 404.
- Verify that the name of the rendered view is 'error/404'.
- Verify that the request is forwarded to url '/WEB-INF/jsp/error/404.jsp'.
The source code of our fourth integration test looks as follows:
import com.github.springtestdbunit.DbUnitTestExecutionListener; import com.github.springtestdbunit.annotation.DatabaseSetup; import com.github.springtestdbunit.annotation.ExpectedDatabase; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.MediaType; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.support.DirtiesContextTestExecutionListener; import org.springframework.test.context.transaction.TransactionalTestExecutionListener; import org.springframework.test.web.server.MockMvc; import org.springframework.test.web.server.samples.context.WebContextLoader; import static org.springframework.test.web.server.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.server.result.MockMvcResultMatchers.*; @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class}) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }) @DatabaseSetup("toDoData.xml") public class ITTodoControllerTest { //Add web application context here private MockMvc mockMvc; //Add setUp() method here @Test @ExpectedDatabase("toDoData.xml") public void updateTodoWhenTodoIsNotFound() throws Exception { mockMvc.perform(post("/todo/update") .contentType(MediaType.APPLICATION_FORM_URLENCODED) .param("description", "description") .param("id", "3") .param("title", "title") .sessionAttr("todo", new TodoDTO()) ) .andExpect(status().isNotFound()) .andExpect(view().name("error/404")) .andExpect(forwardedUrl("/WEB-INF/jsp/error/404.jsp")); } }
Conclusion
We have now learned how we can write integration tests for controller methods that processes form submissions. This tutorial has taught us three things:
- We know how we can specify the content type of the request.
- We know how to send the values of the form fields as request parameters.
- We know how to add values to the session used in our integration test.
- We know how we can check that our form submission has field errors.
In the next part of this tutorial, we learn to write integration tests for a REST API that reads information from the database and returns it to a single page web application.
P.S. You can get the example application of this blog post from Github.
Thanks a lot!
You are welcome!
Very useful, clean and well explained Petri. Really good job!
Hello ! I'm trying to do what in you site, but I can't :-(
http://stackoverflow.com/questions/17073299/bindingresult-doesnt-work-spring-test
Hi,
I assume that you are not using JSR-303 validation since you have not annotated the method parameter with the
@Valid
annotation? Or did you just forget to add it?However, since the validation is working when you run your application and not working when you run a test against your code, the configuration of the application and the configuration of your test might be different.
I would start by checking the configuration files (or classes if you use Java configuration).
Petri,
thanks for answer me :-) do you have an email ? I've been trying to do my test for 3 days and doesn't work. I think you can help me.
thanks
Thanks a lot my friend. I had forgotten to put @Valid ;-) It's work now. I'd like to have your email, I like so much your website, book and explanations. Could you send me an email ? Thanks, thanks, thanks !
You are welcome!
It is good to hear that you were able to solve this problem.
Also, you should have a new email message in your inbox.
Petri,
I'm sorry but now I have other problem.
look,
@Test
@ExpectedDatabase("timeSheetControllerIt.xml")
public void novoTimeSheetSemHoraFinal() throws Exception {
TimeSheet novoTimeSheet = new TimeSheet();
novoTimeSheet.setHoraInicio(calendar.getTime());
mockMvc.perform(
post("/timesheet/addtimesheet")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(super.convertObjectToFormUrlEncodedBytes(novoTimeSheet))
.sessionAttr("timesheet", novoTimeSheet)
)
.andDo(print())
.andExpect(status().isOk())
.andExpect(view().name("timesheetcrud/novo"))
.andExpect(forwardedUrl("/WEB-INF/views/timesheetcrud/novo.jsp"))
.andExpect(model().attributeHasFieldErrors("timesheet", "horaFim"))
.andExpect(model().attribute("messageHoraFimError", "timesheetcontroller.horafim.invalida"));
}
I'm setting "novoTimeSheet.setHoraInicio(calendar.getTime());", but, in my controller:
@RequestMapping(value = "/addtimesheet", method = RequestMethod.POST)
public String addTimeSheet(@Valid @ModelAttribute("timesheet")TimeSheet timeSheet,
BindingResult bindingResult,
ModelMap model) { ...
my attribute is null (timeSheet.getHoraInicio()), in other words, a value of that I'm setting in "novoTimeSheet" does not reach in my controller.
I don't know what I'm doing wrong. TimeSheet is my domain with hibernate annotations. I saw that in your example that you're using DTO.
thanks again :-)
The answer to this problem was that the controller class was not annotated with the
@SessionAttributes
annotation. Maybe this remark will help someone else who is having the same problem.Yes, thanks Petri, the @SessionAttributes issue is exactly the problem I was having too! This is a really helpful tutorial.
You are welcome! I am happy to hear that this tutorial was useful to you.
Yes it was very useful, although something that wasn't clear from the tutorial is that the system only works if your form is rendered by the same controller as the controller that processes the form submission. Worth knowing if @SessionAttributes is giving you trouble! I had my form rendered in ControllerA, but the form was being processed in ControllerB, so it didn't work.
What was the problem in this scenario? The controllerA did not find the form object after it was processed by ControllerB?
I have not personally implemented a form which uses two different controllers but I assume that if you add the
@SessionAttributes
annotation to both controllers, it might work. Did you try this out?Maybe I should could write a separate blog post from this scenario. Did you have some reason to implement the form in this way?
Hi,
Thank you for your blogposts about MockMvc testing. They have been already very useful to me!
I have a small problem, which I can't fix. I googled around a bit but no avail.
On my DTO I have an Object whose properties I wish to test. I thought I could use the following:
.andExpect(model().attribute("myDto", hasProperty("someObject.name", is("test"))));
But I get the following error:
java.lang.AssertionError: Model attribute 'myDto'
Expected: hasProperty("someObject.name", is "test")
but: No property "someObject.name"
Do you have a solution or workaround for this? Or am I doing something else wrong here?
Many thanks,
Frederik
Hi,
You are welcome. I am happy to hear that these blog posts have been useful to you!
About your problem:
I think that the
hasProperty()
matcher does not support nested properties. However, since it accepts a Hamcrest matcher as a second method parameter, it should be possible to chain Hamcrest matchers like this:.andExpect(model().attribute(“myDto”, hasProperty(“someObject”, hasProperty(“name”, is(“test”))));
I have not tested this but it should do the trick. Let me know if it solved your problem.
Yes, it works like that! Thank you very much!
You are welcome!
Hi Wile testing with Mock mvc. I have a problem autowiring bean in custom validators. My custom validator has a autowired dao property which is not loading i am getting null pointer. any thoughts on this?
I have few questions for you:
Is the
NoSuchBeanDefinitionException
thrown or do you just run into NPE in your test? The reason why I ask this is that if the dao bean is not found from the application context of your test, theNoSuchBeanDefinitionException
should be thrown. Are you sure that the dao is null?I assume that the dao property is injected when you run your application. Is this correct?
NPE at customvalidator @autowired property.
Actually i wrote a custom validator which has @autowired Mydao mydao;
mydao used to checks string exits already in the database.
Instead of @Valid in the controller method i have @MyappCheck I am getting NPE while running test case control is going to my customvalidator class but @autowired property showing null when i debug test.
I found an interesting StackOverflow question which might help you out. Let me know if this did the trick.
I tried but It did not worked. Thank you.
Your tutorials have been a godsend. Seems like no one who figures out how to use spring mvc bothers to write a comprehensive guide. Thank you so much. :)
Thank you for your kind words. I am happy to hear that these tutorials were useful to you.
First, thank you for all your posts. They are very informative, their presentation is impeccable and you clearly have well though-out ideas.
I'm reaching out because I'm struggling with some basic questions. Maybe I'm over-thinking things, but here it goes.
We all agree that web applications should have a clean RESTful API. This means addressing resources using proper URIs as much as using appropriate HTTP verbs to describe actions on these resources. While this can be somewhat straight-forward when supporting programmatic clients, the requirement to support HTML browsers complicates things a bit. Take for example your "create Todo" example. What I gather from pure REST principles is that the API should handle a POST on /todos and then return a 201 Created status with a Location: /todos/{id} header. However, when accepting a similar request from a web browser, a human user would expect to be redirected to the Todo list page, and so the API should return a 302 with Location: /todos.
What's a clean API to do in these conditions? Returning different status codes based on the content type doesn't seem to be a good idea. An option would be to return 201 with both a Location header and a Refresh header, but Refresh doesn't seem to be part of the HTTP standard (although supported by every major browser) and AFAIK this practice isn't common place.
What's your take on this?
Thanks a bunch!
First, thank you for your kind words. I really appreciate them.
I guess there are two situations which we must take into consideration:
I hope that this answer helps you to clarify your thoughts.
If you keep seeing status code 400 ( Bad request) it's usually because of the binding error. If you have a large object with multiple attributes it can get frustrating to troubleshoot - Here's a little trick to help decode it.
This will print the bind error so you can figure out what object is causing the problem - for me it was a null value being sent instead of a blank value.
This is a really useful trick. Thanks for sharing!
Petri, just wanted to say thanks for sharing your high quality tutorials. Really good work.
You are welcome! I am happy to hear that these tutorials are useful to you.
Good tutorial Petri, very useful.
But I am getting following error
java.lang.AssertionError: Model attribute 'supplyEmailForm'
Expected: hasProperty("email", is "smunga@gmail.com")
but: was null
I have no idea where I went wrong
This is my test method
This is my controller
Sorry I did not enter my name, it showed as Anonymous and this is my SupplyEmailForm
I am trying to do this test from past 2 days, but no success. Appreciate your help.
problem is at this line .andExpect(model().attribute(“supplyEmailForm”, hasProperty(“email”, is(“smunga@gmail.com”))));
Thanks
hamsa
Hi,
The problem is that your controller creates a new flash attribute, but your test tries to find the form object from the model (naturally this fails because the form object is not there). You can fix this by verifying that a new flash attribute is created by your controller method:
Thanks Petri for the reply.
You are welcome.