Integration Testing of Spring MVC Applications: REST API, Part Two

This is the fifth part of my spring-test-mvc tutorial and it describes the integration testing of a REST API that is implemented by using Spring MVC 3.1. During this tutorial we will continue writing integration tests for a simple REST API that provides CRUD functions for todo entries. This blog entry concentrates on two controller methods that are used to add 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 (Spring Test DBUnit configuration and usage) part of this tutorial.

Lets get started by taking a brief look at the implementation of our example application.

The Anatomy of Our Todo Application

Before we can write integration tests for our todo application, we have to know how it is implemented. This section describes the following parts of our example application:

  • The domain model of the todo application.
  • The data transfer objects that are used to transfer information between the REST API and its clients.
  • The service interface that states the contract between the service layer of the todo application and the controller class.
  • The controller that implements the REST API.

These parts are explained with more details in the following.

The Domain Model

The domain model of our todo application consists of a single entity Todo. The source code of the Todo class looks as follows:

import org.hibernate.annotations.Type;
import org.joda.time.DateTime;

import javax.persistence.*;

@Entity
@Table(name="todos")
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(name = "creation_time", nullable = false)
    @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    private DateTime creationTime;

    @Column(name = "description", nullable = true, length = MAX_LENGTH_DESCRIPTION)
    private String description;

    @Column(name = "modification_time", nullable = false)
    @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
    private DateTime modificationTime;

    @Column(name = "title", nullable = false, length = MAX_LENGTH_TITLE)
    private String title;

    @Version
    private long version;

    public Todo() {

    }

	//Getters and other methods
}

The Data Transfer Objects

Our todo application has three data transfer objects that are described in the following:

  • The TodoDTO class contains the information of a todo entry.
  • The FieldValidationErrorDTO class contains the information of a single validation error.
  • The FormValidationErrorDTO class is a wrapper class that contains a list of validation errors

Lets move on and take a closer look at these data transfer objects.

The TodoDTO Class

The TodoDTO class is a data transfer object which is used to transfer the information of todo entries between the REST API and its clients. The implementation of this DTO is very simple, and its only methods are 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 FieldValidationErrorDTO Class

The FieldValidationErrorDTO class is a data transfer object that contains the information of a single validation error. This class has two properties that described in the following:

  • The path property identifies the property of a todo entry that did not pass the validation phase.
  • The message property contains the actual validation error message.

The source code of the FieldValidationErrorDTO looks as follows:

public class FieldValidationErrorDTO {

    private String path;
    private String message;

    public FieldValidationErrorDTO(String path, String message) {
        this.path = path;
        this.message = message;
    }

	//Getters
}

The FormValidationErrorDTO Class

The FormValidationErrorDTO class is basically a wrapper class that contains a list of FieldValidationErrorDTO objects. Its source code looks as follows:

public class FormValidationErrorDTO {

    private List<FieldValidationErrorDTO> fieldErrors = new ArrayList<FieldValidationErrorDTO>();

    public FormValidationErrorDTO() {

    }

    public void addFieldError(String path, String message) {
        FieldValidationErrorDTO fieldError = new FieldValidationErrorDTO(path, message);
        fieldErrors.add(fieldError);
    }

    //Getter
}

The Service Interface

The TodoService interface describes the contract between our controller and the service layer. This interface declares two methods that are described in the following:

  • The Todo add(TodoDTO added) method adds a new todo entry and returns the added todo entry.
  • The Todo update(TodoDTO updated) method updates the information of a todo entry and returns the updated todo entry. If no todo entry is found, this method throws a 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 Web Layer

The section describes the web layer of our todo application. To be more exact, this section describes both the custom exception that is thrown when the validation fails and the implementation of our controller.

The FormValidationError Class

The FormValidationError class is an exception that is thrown by our controller if the validation of the added or updated todo entry fails. This class contains a list of FieldError objects and its source code looks as follows:

import org.springframework.validation.FieldError;

import java.util.List;

public class FormValidationError extends Exception {

    private List<FieldError> fieldErrors;

    public FormValidationError(List<FieldError> fieldErrors) {
        this.fieldErrors = fieldErrors;
    }

    //Getter
}

The Controller

Our controller class is called TodoController and its methods are described in the following:

  • The TodoDTO add(@RequestBody TodoDTO dto) method adds a new todo entry and returns the information of the added todo entry.
  • The TodoDTO update(@RequestBody TodoDTO dto, @PathVariable(“id”) Long todoId) method updates the information of a todo entry and returns the updated information.
  • The void handleTodoNotFoundException(TodoNotFoundException ex) method is an exception handler method that returns 404 HTTP status code when a TodoNotFoundException is thrown from the service layer of our todo application.
  • The FormValidationErrorDTO handleFormValidationError(FormValidationError validationError) method is an exception handler method that handles validation errors. It returns 400 HTTP status code. The actual validation errors are added to a new FormValidationErrorDTO object that is also returned.

The source code of the TodoController class looks as follows:

import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.Validator;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;
import java.util.Locale;

@Controller
public class TodoController {

    @Resource
    private TodoService service;

    @Resource
    private LocaleContextHolderWrapper localeHolderWrapper;

    @Resource
    private MessageSource messageSource;

    @Resource
    private Validator validator;


    @RequestMapping(value = "/api/todo", method = RequestMethod.POST)
    @ResponseBody
    public TodoDTO add(@RequestBody TodoDTO dto) throws FormValidationError {
        validate("todo", dto);

        Todo added = service.add(dto);

        return createDTO(added);
    }

    @RequestMapping(value = "/api/todo/{id}", method = RequestMethod.PUT)
    @ResponseBody
    public TodoDTO update(@RequestBody TodoDTO dto, @PathVariable("id") Long todoId) throws TodoNotFoundException, FormValidationError {
        validate("todo", dto);

        Todo updated = service.update(dto);

        return createDTO(updated);
    }

    private TodoDTO createDTO(Todo model) {
        TodoDTO dto = new TodoDTO();

        dto.setId(model.getId());
        dto.setDescription(model.getDescription());
        dto.setTitle(model.getTitle());

        return dto;
    }

    private void validate(String objectName, Object validated) throws FormValidationError {
        BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(validated, objectName);
        validator.validate(validated, bindingResult);

        if (bindingResult.hasErrors()) {
            throw new FormValidationError(bindingResult.getFieldErrors());
        }
    }

    @ExceptionHandler(FormValidationError.class)
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public FormValidationErrorDTO handleFormValidationError(FormValidationError validationError) {
        Locale current = localeHolderWrapper.getCurrentLocale();

        List<FieldError> fieldErrors = validationError.getFieldErrors();

        FormValidationErrorDTO dto = new FormValidationErrorDTO();

        for (FieldError fieldError: fieldErrors) {
            String[] fieldErrorCodes = fieldError.getCodes();
            for (int index = 0; index < fieldErrorCodes.length; index++) {
                String fieldErrorCode = fieldErrorCodes[index];

                String localizedError = messageSource.getMessage(fieldErrorCode, fieldError.getArguments(), current);
                if (localizedError != null && !localizedError.equals(fieldErrorCode)) {
                    dto.addFieldError(fieldError.getField(), localizedError);
                    break;
                }
                else {
                    if (isLastFieldErrorCode(index, fieldErrorCodes)) {
                        dto.addFieldError(fieldError.getField(), localizedError);
                    }
                }
            }
        }

        return dto;
    }

    private boolean isLastFieldErrorCode(int index, String[] fieldErrorCodes) {
        return index == fieldErrorCodes.length - 1;
    }

    @ExceptionHandler(TodoNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public void handleTodoNotFoundException(TodoNotFoundException ex) {
    }
}

Writing Integration Tests

This section describes how we can write integration tests for our REST API. Lets start by taking a look at common testing utilities which we will use in our integration tests.

Common Testing Utilities

We use three testing utilities in our integration tests. These utilities are described in the following:

  • The TodoTestUtil class is used in the unit and integration tests of our todo application.
  • The IntegrationTestUtil class is used in our integration tests.
  • The DBUnit dataset file is used to initialize our database to a known state before our tests are run.

These utilities are described with more details in the following.

The TodoTestUtil Class

The TodoTestUtil class has two static methods which are used in our integration tests. These methods are described in the following:

  • The TodoDTO createDTO(Long id, String description, String title) method creates new TodoDTO objects.
  • The String createStringWithLength(int length) methods creates new String objects.

The source code of the TodoTestUtil looks as follows:

public class TodoTestUtil {

    public static TodoDTO createDTO(Long id, String description, String title) {
        TodoDTO dto = new TodoDTO();

        dto.setId(id);
        dto.setDescription(description);
        dto.setTitle(title);

        return dto;
    }

    public static String createStringWithLength(int length) {
        StringBuilder builder = new StringBuilder();

        for (int index = 0; index < length; index++) {
            builder.append("a");
        }

        return builder.toString();
    }
}

The IntegrationTestUtil Class

The IntegrationTestUtil class has two purposes:

First, It declares a constant called APPLICATION_JSON_UTF8 that is later used to set the content type and character set of HTTP requests and verify the content type and the character set of HTTP responses.

Second, It contains a byte[] convertObjectToJsonBytes(Object object) method that is used to convert objects to bytes that contains the JSON representation of the object given as a parameter. The implementation of this method has following steps:

  1. Create a new ObjectMapper object.
  2. Configure the created object to include only non null properties of the serialized object.
  3. Convert the object as json string and return the created string as byte array.

The source code of the IntegrationTestUtil class looks as follows:

import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.springframework.http.MediaType;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

public class IntegrationTestUtil {

    public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));

    public static byte[] convertObjectToJsonBytes(Object object) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
        return mapper.writeValueAsBytes(object);
    }
}

The DBUnit Dataset

Each integration test use the same DBUnit dataset file to initialize the database to a known state before tests are run. The name of our dataset file is toDoData.xml and its content 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>

Add Todo Entry

As we know, the add() method of the TodoController class is used to add new todo entries. We have to write three integration tests for this method:

  • We must write a test that ensures the we cannot add an empty todo entry.
  • We must write a test that ensures that we cannot add todo entry when its title and description are too long.
  • We must write a test that ensures that we can add todo entries.

These tests are explained with more details in the following.

Add Empty Todo Entry

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. Create a new TodoDTO object with empty title and description.
  3. Perform a POST request to url '/api/todo'. Set the content type of the request to 'application/json'. Set the character set of the request to 'UTF8'. Transform the created object into a correct format and send it in the body the request.
  4. Verify that the 400 HTTP status code is returned.
  5. Verify that the content type of the response is 'application/json' and its character set is 'UTF8'.
  6. Verify that the body of the response contains a validation error about empty title.

The source code of this 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.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@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 {
        TodoDTO added = TodoTestUtil.createDTO(null, "", "");
        mockMvc.perform(post("/api/todo")
                .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
                .body(IntegrationTestUtil.convertObjectToJsonBytes(added))
        )
                .andExpect(status().isBadRequest())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string("{\"fieldErrors\":[{\"path\":\"title\",\"message\":\"The title cannot be empty.\"}]}"));
    }
}

Add Todo Entry 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 description of the todo entry.
  3. Create a new TodoDTO object, and set its title and description.
  4. Perform a POST request to url '/api/todo'. Set the content type of the request to 'application/json'. Set the character set of the request to 'UTF8'. Transform the created object into a correct format and send it in the body of the request.
  5. Verify that the 400 HTTP status code is returned.
  6. Verify that the content type of the response is 'application/json' and its character set is 'UTF8'.
  7. Verify that the response body contains validation errors for both title and description fields.

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.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@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);
        TodoDTO added = TodoTestUtil.createDTO(null, description, title);

        mockMvc.perform(post("/api/todo")
                .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
                .body(IntegrationTestUtil.convertObjectToJsonBytes(added))
        )
                .andExpect(status().isBadRequest())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string(startsWith("{\"fieldErrors\":[")))
                .andExpect(content().string(allOf(
                        containsString("{\"path\":\"description\",\"message\":\"The maximum length of the description is 500 characters.\"}"),
                        containsString("{\"path\":\"title\",\"message\":\"The maximum length of the title is 100 characters.\"}")
                )))
                .andExpect(content().string(endsWith("]}")));
    }
}

Add Todo Entry

We can write the third integration test by following these steps:

  1. Use the @ExpectedDatabase annotation to ensure that a new todo entry is added to the used database.
  2. Create a new TodoDTO object with valid title and description.
  3. Perform a POST request to url '/api/todo'. Set the content type of the request to 'application/json'. Set the character set of the request to 'UTF8'. Transform the created object into a correct format and send it in the body of the request.
  4. Verify that the 200 HTTP status code is returned.
  5. Verify that the content type of the response is 'application/json' and its character set is 'UTF8'.
  6. Verify that the information of the added todo entry is returned.

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 com.github.springtestdbunit.assertion.DatabaseAssertionMode;
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.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@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 add() throws Exception {
        TodoDTO added = TodoTestUtil.createDTO(null, "description", "title");
        mockMvc.perform(post("/api/todo")
                .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
                .body(IntegrationTestUtil.convertObjectToJsonBytes(added))
        )
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string("{\"id\":3,\"description\":\"description\",\"title\":\"title\"}"));
    }
}

The name of the DBUnit dataset file that is used to verify that the todo entry was added is toDoData-add-expected.xml. The content of this file looks as follows:

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

Update Todo Entry

As we learned earlier, the controller method that is used to update the information of existing todo entries is called update(). We have to write four integration tests for this method:

  • We have to write a test that ensures that we cannot update empty todo entry.
  • We have to write a test that ensures that we cannot update the information of a todo entry when its title and description are too long.
  • We have to write a test that ensures that 404 HTTP status code is returned when the updated todo entry is not found.
  • We have to write a test that ensures that we can update the information of a todo entry.

These tests are described with more details in the following:

Update Empty Todo Entry

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. Create a new TodoDTO object and set the value of its id.
  3. Perform a PUT request to url '/api/todo/1'. Set the content type of the request to 'application/json'. Set the character set of the request to 'UTF8'. Transform the created object into a correct format and send it in the body the request.
  4. Verify that the 400 HTTP status code is returned.
  5. Verify that the content type of the response is 'application/json' and its character set is 'UTF8'.
  6. Verify that the body of the response contains a validation error about empty title.

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 com.github.springtestdbunit.assertion.DatabaseAssertionMode;
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.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@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 {
        TodoDTO updated = TodoTestUtil.createDTO(1L, "", "");

        mockMvc.perform(put("/api/todo/{id}", 1L)
                .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
                .body(IntegrationTestUtil.convertObjectToJsonBytes(updated))
        )
                .andExpect(status().isBadRequest())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string("{\"fieldErrors\":[{\"path\":\"title\",\"message\":\"The title cannot be empty.\"}]}"));
    }
}

Update Todo Entry with Validation Errors

We can write the second integration test by following these steps:

  1. Use the @ExpectedDatabase annotation to verify that no changes are made to the database.
  2. Create the title and description of the todo entry.
  3. Create a new TodoDTO object and set the value of its id, title and description.
  4. Perform a PUT request to url '/api/todo/1'. Set the content type of the request to 'application/json'. Set the character set of the request to 'UTF8'. Transform the created object into a correct format and send it in the body the request.
  5. Verify that the 400 HTTP status code is returned.
  6. Verify that the content type of the response is 'application/json' and its character set is 'UTF8'.
  7. Verify that the body of the response contains a validation error about empty title.

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 com.github.springtestdbunit.assertion.DatabaseAssertionMode;
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.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@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);

        TodoDTO updated = TodoTestUtil.createDTO(1L, description, title);

        mockMvc.perform(put("/api/todo/{id}", 1L)
                .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
                .body(IntegrationTestUtil.convertObjectToJsonBytes(updated))
        )
                .andExpect(status().isBadRequest())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string(startsWith("{\"fieldErrors\":[")))
                .andExpect(content().string(allOf(
                        containsString("{\"path\":\"description\",\"message\":\"The maximum length of the description is 500 characters.\"}"),
                        containsString("{\"path\":\"title\",\"message\":\"The maximum length of the title is 100 characters.\"}")
                )))
                .andExpect(content().string(endsWith("]}")));
    }
}

Update Todo Entry when Todo Entry Is Not Found

We can write the third integration test by following these steps:

  1. Use the @ExpectedDatabase annotation to verify that no changes are made to the database.
  2. Create a new TodoDTO object and set the value of its id, title and description. Ensure that no todo entry is found with the given id.
  3. Perform a PUT request to url '/api/todo/3'. Set the content type of the request to 'application/json'. Set the character set of the request to 'UTF8'. Transform the created object into a correct format and send it in the body the request.
  4. Verify that the 404 HTTP status code is returned.

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.springframework.test.web.server.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@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 {
        TodoDTO updated = TodoTestUtil.createDTO(3L, "description", "title");

        mockMvc.perform(put("/api/todo/{id}", 3L)
                .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
                .body(IntegrationTestUtil.convertObjectToJsonBytes(updated))
        )
                .andExpect(status().isNotFound());
    }
}

Update Todo Entry

We can write the fourth integration test by following these steps:

  1. Use the @ExpectedDatabase annotation to verify that the information of the todo entry is updated.
  2. Create a new TodoDTO object and set the value of its id, title and description.
  3. Perform a PUT request to url '/api/todo/1'. Set the content type of the request to 'application/json'. Set the character set of the request to 'UTF8'. Transform the created object into a correct format and send it in the body the request.
  4. Verify that the 200 HTTP status code is returned.
  5. Verify that the content type of the response is 'application/json' and its character set is 'UTF8'.
  6. Verify that the information of the updated todo entry is returned.

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 com.github.springtestdbunit.assertion.DatabaseAssertionMode;
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.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;

@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 update() throws Exception {
        TodoDTO updated = TodoTestUtil.createDTO(1L, "description", "title");

        mockMvc.perform(put("/api/todo/{id}", 1L)
                .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
                .body(IntegrationTestUtil.convertObjectToJsonBytes(updated))
        )
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string("{\"id\":1,\"description\":\"description\",\"title\":\"title\"}"));
    }
}

The name of the DBUnit dataset file that is used to verify that the information of the todo entry was updated is toDoData-update-expected.xml. The content of this file looks as follows:

<dataset>
    <todos id="1" description="description" title="title" version="1"/>
    <todos id="2" description="Lorem ipsum" title="Bar" version="0"/>
</dataset>

What is Next?

We have now learned how we can write integration tests for a REST API methods that can be used to add information to a database and update the existing information. This tutorial has taught us two things:

  • We know how we can set content type of a HTTP request.
  • We know how we can convert objects into json and send it in the body a HTTP request.

In the next part of this tutorial, we learn to write integration tests which ensure that our application is secured correctly. You might also want to find out how you can clean up your tests by using JsonPath expressions.

P.S. You can get the example application of this blog post from Github.

If you want to save time by writing less test code, take a look at my upcoming Test With Spring Course.
19 comments… add one
  • 打倒日本帝国主义 Mar 22, 2013 @ 11:23

    写的很好,非常感谢!

    • Petri Mar 22, 2013 @ 12:00

      You are welcome!

  • Maksim Mar 7, 2015 @ 17:34

    Petri, that is just brilliant article. Thanks for this.
    Could you highlight your thought on the situation when todo entry contains a file (org.springframework.web.multipart.MultipartFile). So the client submits a DTO containing MultipartFile:

    
    public class TodoDTO {
        private Long id;
        private String description;
        private MultipartFile image;
        ...
    }
    
    

    For sure the controller won't return the same DTO because MultipartFile is something for incoming data not outgoing. So how can the controller be rewritten then? It cannot return the same DTO now.

    
    @RequestMapping(value = "/api/todo", method = RequestMethod.POST) @ResponseBody
    public TodoDTO add(@RequestBody TodoDTO dto) throws FormValidationError { ... }
    
    

    Thanks in advance!

    • Petri Mar 7, 2015 @ 20:39

      Hi Maksim,

      Thank you for your kind words. I am happy to hear that this blog post was useful to you! Also, thank you for asking such a good question. Here are my thoughts about your problem:

      I would split that operation into two separate operations:

      • The first operation would create a new todo entry and return the information of the created todo entry.
      • The second operation would upload the file and attach it to the correct todo entry. This operation could return a DTO that contains the name of the uploaded file and other information that is required by the client.

      If this is not possible, you have two choices:

      • You can use two different data transfer objects. The first DTO would be used by create a new todo entry (this DTO would have a MultipartFile field). The second DTO would be used to return the information of the created todo entry (this DTO wouldn't have a MultipartFile field).
      • You can use the same DTO and simply set its MultipartFile field to null when you are returning the information of the created todo entry. If you have configured Jackson to ignore null fields, the client will never see the MultipartFile field because it is ignored by Jackson.

      I think that the answer to you question depends from personal preferences. If it is not possible to split the operation into two separate operations, you have to make a tradeoff between writing more code (using two DTOs) and using one DTO that has unused field. Select the option that makes sense to you.

      • Maksim Mar 7, 2015 @ 22:13

        Petri, thanks, that is more than exhaustive answer!
        You're right, it depends on the service. Because in my case the client needs to know the image name, I'll go with the option using two different data transfer objects.

        • Petri Mar 8, 2015 @ 16:31

          You are welcome! I am happy to hear that my answer was useful to you.

          To be honest, it is often kind of hard to give good advice because most of the time I have no idea what my reader is trying to accomplish. That is why I don't want to have the final say. My job is to provide a few options and let my reader select the best one.

  • Emiliano Jun 13, 2015 @ 16:32

    Hi Petri. Excellent tutorial, really.
    What about testing a rest method that returns void?
    I´m receiving this assertion error expecting a 200 status:
    java.lang.AssertionError: Status expected: but was:
    How can I tell the mock, that there is no response content.

    Thanks in advance and keep going!

    • Petri Jun 13, 2015 @ 17:15

      Hi Emiliano,

      Thank you for your kind words. I really appreciate them.

      Have you annotated the tested method with the @ResponseBody or @ResponseStatus annotation? If you haven't done this, you can solve your problem by using one of these options:

      • Return HTTP response status 200 by annotating the controller method with the @ResponseBody annotation.
      • If you want to return another HTTP response status, you can configure the returned response status by annotating the controller method with the @ResponseStatus annotation.

      This Stackoverflow question provides more details about this situation.

  • Dominik Nov 13, 2015 @ 10:57

    Hey, first of all thanks a lot for your tutorials!
    I have some questions left how to get DTOs and entity relationships integrated within the service layer. Assuming we have another entity like Person and its PersonDTO. When I create a new Todo with a TodoService I want to add a relation to a Person.

    Should I directly use the PersonRepository to the JPA entity object or the PersonService to get its DTO? The latter one seems cleaner to me as I do not circumvent any logic that might be in the PersonService. I cannot see how to get the Todo entity linked to a Person with its DTO. Do maybe have an example in your tutorials I just couldn't find so far?

    • Petri Nov 16, 2015 @ 22:08

      Hi Dominik,

      If you are writing an application from a scratch, I think that it makes more sense to add the person's id to the Todo object and fetch the actual Person object by using a separate finder / service class. The reason for this that I don't see how the Person object could belong to the same aggregate than the Todo object.

      When I create a new Todo with a TodoService I want to add a relation to a Person. I cannot see how to get the Todo entity linked to a Person with its DTO. Do maybe have an example in your tutorials I just couldn’t find so far?

      I would create this function by following these steps:

      1. Create an application service class.
      2. Create a transactional method which saves a new todo entry to the database and returns a TodoDTO object. Let's call this method add().
      3. Find the Person object by using its id (this must be given when a new todo entry is created).
      4. If the Person object is found, add its id to the created Todo object and save its information to the database.
      5. Create the returned TodoDTO object by using the found Person object and the created Todo object.
      6. Return the TodoDTO object.

      If you have any additional questions, don't hesitate to ask them.

      • Hong Jan 11, 2016 @ 8:29

        Hi Petri,does it mean not to add @ManyToOne and @OneToMany in Todo entity and People entity? just add the Long id to the ToDo entity?I am very puzzled.Thank you!

        • Petri Jan 11, 2016 @ 21:28

          Hi Hong,

          it depends.

          If you want to use the traditional way, a Todo object would have a reference to a Person object. The problem of this approach is that it's kind of hard (and a bit pointless) to modularize your code because your entities form "a net" that can be used to find (almost) any entity found from your domain model.

          That is why I think that it is better to use domain-driven design, bounded contexts, and aggregates. If you choose to do so, then you need only the id of the person in question.

          • Hong Jan 12, 2016 @ 13:24

            Petri, thanks.I think you means that I can fetch the Person object in the service layer,and the traditional way fetch the Person object in the entities? If I have Product and ProductType,and I think they belong to the aggregate(may be I am wrong),then I can use the @ManyToOne ProductType in the Product entity.Or if I decide to use domain-driven design ,I should not use any annotationed entity property in entity?

          • Petri Jan 13, 2016 @ 19:55

            Hi Hong,

            You understood correctly. If you want to use DDD and you decide that Todo and Person do not belong to the same aggregate, you can fetch the Person object on the service layer. This means that you should add only the id of the Person into the Todo class.

            If you use the traditional way, typically the Person object is obtained by using a getter method that is found from the Todo class. This naturally means that a Todo object must have a reference to a Person object.

          • Hong Jan 14, 2016 @ 15:54

            Thank you ,Petri.I think I get it(may be).Your website is awesome! I can find something from it to help me understand DDD and other useful things that I don't konw.

          • Petri Jan 14, 2016 @ 21:17

            Thank you for your kind words. I really appreciate them. By the way, I am not sure if I understand DDD, but I think that you should not let your doubts stop you. Just try different things and use things that work for you (and ditch the rest).

  • Menuka Jan 16, 2017 @ 7:03

    Hellow Petri
    Internet state that hamcrest dependencies as deprecated. Is there a alternative dependency for hamcrest?

    • Petri Jan 16, 2017 @ 9:40

      Hi,

      As far as I know, Hamcrest has not been deprecated. Also, I did a fast Google search and didn't find anything about this. Could you share a link to the web page which says otherwise (I want to know if it has been deprecated / abandoned)?

      In any case, at the moment there is no alternative since Spring MVC Test supports only Hamcrest matchers.

Leave a Reply