Do want to get a better understanding of Spring web application architecture? If so, get started right now!

Integration Testing of Spring MVC Applications: Forms

The final piece of the puzzle

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.

This blog entry assumes that we are familiar with the concepts described in the first and second part of this tutorial.

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:

  1. Declare the jackson-core-asl (version 1.9.9) dependency in our pom.xml file.
  2. 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:

  1. Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
  2. Perform a GET request to url ‘/todo/add’.
  3. Verify that the HTTP status code is 200.
  4. Verify that name of the rendered view is ‘todo/add’.
  5. Verify that the request is forwarded to url ‘/WEB-INF/jsp/todo/add.jsp’.
  6. 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:

  1. Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
  2. Perform a POST request to url ‘/todo/add’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  3. Verify that the HTTP status code is 200.
  4. Verify that the name of the rendered view is ‘/todo/add’.
  5. Verify that the request is forwarded to url ‘/WEB-INF/jsp/todo/add.jsp’.
  6. Verify that there is a field error in the title field.
  7. 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:

  1. Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
  2. Create title and description of the todo entry.
  3. Perform a POST request to url ‘/todo/add’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Send the description and title of the todo entry as request parameters.
    3. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  4. Verify that the HTTP status code is 200.
  5. Verify that the name of the rendered view is ‘/todo/add’.
  6. Verify that the request is forwarded to url ‘/WEB-INF/jsp/todo/add.jsp’.
  7. Verify that there are a field errors in the title and description fields.
  8. 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:

  1. Use the @ExpectedDatabase annotation to verify that a new todo entry is added to database.
  2. Perform a POST request to url ‘/todo/add’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Send the description and title of the todo entry as request parameters.
    3. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  3. Verify that the HTTP status code is 200.
  4. Verify that the name of the rendered view is ‘redirect:/todo/view/{id}’.
  5. Verify that the model has an attribute caled ‘id’ and that its value is 3.
  6. 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:

  1. Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
  2. Perform a GET request to url ‘/todo/update/1′.
  3. Verify that the HTTP status code is 200.
  4. Verify that the name of the rendered view is ‘todo/update’.
  5. Verify that the request is forwarded to url ‘/WEB-INF/jsp/todo/update.jsp’.
  6. 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:

  1. Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
  2. Perform a GET to request to url ‘/todo/update/3′.
  3. Verify that the HTTP status code is 404.
  4. Verify that the name of the rendered view is ‘error/404′.
  5. 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:

  1. Use the @ExpectedDatabase annotation to verify that no changes are made to the database.
  2. Perform a POST request to url ‘/todo/update’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Send the id of the todo entry as a request parameter
    3. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  3. Verify that the HTTP status code is 200.
  4. Verify that the name of the rendered view is ‘/todo/update’.
  5. Verify that the request is forwarded to url ‘/WEB-INF/jsp/todo/update.jsp’.
  6. Verify that there is a field error in the title field.
  7. 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:

  1. Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
  2. Create the title and the description of the todo entry.
  3. Perform a POST request to url ‘/todo/update’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Send the description, id, and title of the todo entry as request parameters
    3. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  4. Verify that the HTTP status code is 200.
  5. Verify that the name of the rendered view is ‘/todo/update’.
  6. Verify that the request is forwarded to url ‘/WEB-INF/jsp/todo/update.jsp’.
  7. Verify that there are field errors in the title and description fields.
  8. 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:

  1. Use the @ExpectedDatabase annotation to verify that the information of the todo entry is updated.
  2. Perform a POST request to url ‘/todo/update’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Send the description, id, and title of the todo entry as request parameters
    3. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  3. Verify that the HTTP status code is 200.
  4. Verify that the name of the rendered view is ‘redirect:/todo/view/{id}’.
  5. Verify that the model has an attribute ‘id’ and that its value is 1.
  6. 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:

  1. Use the @ExpectedDatabase annotation to verify that no changes is made to the database.
  2. Perform a POST request to url ‘/todo/update’ by following these steps:
    1. Set the content type of the request to ‘application/x-www-form-urlencoded’.
    2. Send the description, id, and title of the todo entry as request parameters
    3. Set a new TodoDTO object to session. This is required because our controller is annotated with the @SessionAttributes annotation.
  3. Verify that the HTTP status code is 404.
  4. Verify that the name of the rendered view is ‘error/404′.
  5. 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. The source code of our todo application is available at Github.

If you want to learn more about Spring MVC Test, you should read my Spring MVC Test tutorial.

About the Author

Petri Kainulainen is passionate about software development and continuous improvement. He is specialized in software development with the Spring Framework and is the author of Spring Data book.

About Petri Kainulainen →

31 comments… add one

  • Thanks a lot!

    Reply
    • You are welcome!

      Reply
  • Very useful, clean and well explained Petri. Really good job!

    Reply
  • Hello ! I’m trying to do what in you site, but I can’t :-(

    http://stackoverflow.com/questions/17073299/bindingresult-doesnt-work-spring-test

    Reply
    • 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).

      Reply
  • 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

    Reply
  • 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 !

    Reply
    • 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.

      Reply
  • 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 :-)

    Reply
    • 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.

      Reply
  • Yes, thanks Petri, the @SessionAttributes issue is exactly the problem I was having too! This is a really helpful tutorial.

    Reply
    • You are welcome! I am happy to hear that this tutorial was useful to you.

      Reply
      • 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.

        Reply
        • 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?

          Reply
  • 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

    Reply
    • 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.

      Reply
      • Yes, it works like that! Thank you very much!

        Reply
        • You are welcome!

          Reply
  • 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?

    Reply
    • 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, the NoSuchBeanDefinitionException 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?

      Reply
  • 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.

    Reply
  • I tried but It did not worked. Thank you.

    Reply
  • 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. :)

    Reply
    • Thank you for your kind words. I am happy to hear that these tutorials were useful to you.

      Reply
  • 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!

    Reply
    • First, thank you for your kind words. I really appreciate them.

      I guess there are two situations which we must take into consideration:

      1. The backend of our application has only a REST API which is used by programmatic clients (one of these clients is a single page web application). In this scenario it seems pretty clear that the HTTP status code 201 should be returned when a new todo entry is created.
      2. We have a “normal” Spring MVC application which offers a REST API for programmatic clients. This is the trickier scenario, and I would probaby solve this problem by returning the HTTP status code 201 when a new todo entry is created by using the REST API. I agree that returning different status codes is confusing but I suspect that the user of our “normal” web application doesn’t really care about the returned HTTP status code(he might not even know what a HTTP status code is). He just wants to be sure that a new todo entry was created. That is why I think that returning different HTTP status is acceptable in this case.

      I hope that this answer helps you to clarify your thoughts.

      Reply
  • 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.

    
    MvcResult result = mockMvc.perform(post("/todo/update")
    		.contentType(MediaType.APPLICATION_FORM_URLENCODED)
    		.param("description", "description")
    		.param("id", "3")
    		.param("title", "title")
    		.sessionAttr("todo", new TodoDTO())).andReturn();
    
    result.getResolvedException().printStackTrace();
    
    

    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.

    Reply
    • This is a really useful trick. Thanks for sharing!

      Reply
  • Petri, just wanted to say thanks for sharing your high quality tutorials. Really good work.

    Reply
    • You are welcome! I am happy to hear that these tutorials are useful to you.

      Reply

Leave a Comment