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

This is the fourth part of my spring-test-mvc tutorial and it describes how we can write integration tests for a REST API that is implemented by using Spring MVC 3.1. During this tutorial we write integration tests for a simple REST API that provides CRUD functions for todo entries. This blog entry concentrates on three controller methods that are used to get a list of todo entries, get the information of a single todo entry and delete todo entries from the database.

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 move on and take 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 take a quick look at its implementation. This section describes the following parts of our todo application:

  • The domain model of the todo application.
  • The service interface that describes the contract between our controller and the service layer of the application.
  • The data transfer object that is used to transfer information between our REST API and its clients.
  • The controller that implements the REST API.

Lets move on and find out how our example application is implemented.

The Domain Model

The domain model of our todo application consists of a single entity called 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 = 500)
    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 = 100)
    private String title;

    @Version
    private long version;

	public Todo() {

	}

	//Getters and other methods
}

The Service Interface

In order to understand the implementation of our controller, we must understand the contract between the controller and the service layer. The TodoService interface describes this contract and it declares the following methods:

  • The Todo deleteById(Long id) method deletes a todo entry by using the id given as parameter. If the deleted todo entry is not found, this method throws TodoNotFoundException.
  • The List<Todo> findAll() method returns a list of todo entries. If no todo entries is found, this method returns an empty list.
  • The Todo findById(Long id) method returns a todo entry by using the id given as a parameter as a search criteria. 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 deleteById(Long id) throws TodoNotFoundException;

    public List<Todo> findAll();

    public Todo findById(Long id) throws TodoNotFoundException;
}

The Data Transfer Object

Our todo application has a single data transfer object which is used transfer information between our REST API and its clients. The implementation of this DTO is very simple. It has a few fields, and its only methods are getters and setters. The source code of the TodoDTO class looks as follows:

public class TodoDTO {

    private Long id;
    private String description;
    private String title;

    public TodoDTO() {

    }

	//Getters and setters
}

The Controller

The name of our controller class is TodoController and its methods are described in the following:

  • The TodoDTO deleteById(Long id) method deletes a todo entry and returns the information of the deleted todo entry.
  • The List<TodoDTO> findAll() method returns the information of todo entries.
  • The TodoDTO findById(Long id) method method returns the information of a todo entry.
  • The void handleTodoNotFoundException(TodoNotFoundException ex) method is an exception handler method that returns the 404 HTTP status code when a TodoNotFoundException is thrown from the service layer of our todo application.

The source code of the TodoController class looks as follows:

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@Controller
public class TodoController {

    @Resource
    private TodoService service;

    @RequestMapping(value = "/api/todo/{id}", method = RequestMethod.DELETE)
    @ResponseBody
    public TodoDTO deleteById(@PathVariable("id") Long id) throws TodoNotFoundException {
        Todo deleted = service.deleteById(id);
        return createDTO(deleted);
    }

    @RequestMapping(value = "/api/todo", method = RequestMethod.GET)
    @ResponseBody
    public List<TodoDTO> findAll() {
        List<Todo> models = service.findAll();
        return createDTOs(models);
    }

    private List<TodoDTO> createDTOs(List<Todo> models) {
        List<TodoDTO> dtos = new ArrayList<TodoDTO>();

        for (Todo model: models) {
            dtos.add(createDTO(model));
        }

        return dtos;
    }

    @RequestMapping(value = "/api/todo/{id}", method = RequestMethod.GET)
    @ResponseBody
    public TodoDTO findById(@PathVariable("id") Long id) throws TodoNotFoundException {
        Todo found = service.findById(id);
        return createDTO(found);
    }

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

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

        return dto;
    }

    @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. However, before we can start writing these tests, we have to to take a look at some common testing utilities which we will use in our integration tests.

Common Testing Utilities

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

  • The IntegrationTestUtil class is used in our integration tests.
  • A 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 IntegrationTestUtil Class

We have added a constant to the IntegrationTestUtil class that is later used to verify the content type and the character set of the HTTP response. The source code of the IntegrationTestUtil class looks as follows:

import org.springframework.http.MediaType;
import java.nio.charset.Charset;

public class IntegrationTestUtil {

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

}

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>

Get Todo List

The controller method that is used to get the information of all todo entries is called findAll(). We can write an integration test for this method by following these steps:

  1. Use the @ExceptedDatabase annotation to verify that no changes are made to the database.
  2. Perform a GET request to url '/api/todo'.
  3. Verify that the HTTP status code is 200.
  4. Verify that the the content type of the response is 'application/json' and its character set is 'UTF8'
  5. Verify that the information of the found todo entries 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.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 findAll() throws Exception {
        mockMvc.perform(get("/api/todo"))
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string("[{\"id\":1,\"description\":\"Lorem ipsum\",\"title\":\"Foo\"},{\"id\":2,\"description\":\"Lorem ipsum\",\"title\":\"Bar\"}]"));
    }
}

Get Todo Entry

As we learned earlier, the findById() method of the TodoController class is used to get the information of a todo entry. We have to write two integration tests for this method:

  • We must write a test that ensures that the information of a todo entry is returned when the todo entry is found.
  • We must write a test that ensures that the 404 HTTP status code is returned when the todo entry is not found.

These tests are described with more details in the following.

Get Todo Entry When Todo Entry Is Found

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 '/api/todo/1'.
  3. Verify that the HTTP status code is 200.
  4. Verify that the the content type of the response is 'application/json' and its character set is 'UTF8'
  5. Verify that the information of the correct todo entry is returned

The source code of the 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.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 findById() throws Exception {
        mockMvc.perform(get("/api/todo/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string("{\"id\":1,\"description\":\"Lorem ipsum\",\"title\":\"Foo\"}"));
    }
}

Get Todo Entry When Todo Entry Is Not Found

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. Perform a GET request to url '/api/todo/3'.
  3. Verify that the returned HTTP status code is 404.

The source code of the second integration tests 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 findByIdWhenTodoIsNotFound() throws Exception {
        mockMvc.perform(get("/api/todo/{id}", 3L))
                .andExpect(status().isNotFound());
    }
}

Delete Todo Entry

As we know, the controller method that is used to delete the information of a todo entry is called deleteById(). We have to write two integration tests for this method:

  • We have to ensure that the found todo entry is deleted and its information is returned correctly.
  • We have to ensure that the 404 HTTP status code is returned if no todo entry is found.

These tests are described with more details in the following.

Delete Todo Entry When Todo Entry Is Found

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

  1. Use the @ExpectedDatabase annotation to ensure that the todo entry is deleted.
  2. Perform a DELETE request to url '/api/todo/1'.
  3. Verify that the return HTTP status code is 200.
  4. Verify that the content type of the response is 'application/json' and its character set is 'UTF8'.
  5. Verify that the information of the deleted todo entry is returned.

The source code of the 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.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-delete-expected.xml")
    public void deleteById() throws Exception {
        mockMvc.perform(delete("/api/todo/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(content().string("{\"id\":1,\"description\":\"Lorem ipsum\",\"title\":\"Foo\"}"));
    }
}

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

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

Delete Todo Entry When 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 DELETE request to url '/api/todo/3'.
  3. Verify that the returned HTTP status code is 404.

The source code of the 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.*;
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 deleteByIdWhenTodoIsNotFound() throws Exception {
        mockMvc.perform(delete("/api/todo/{id}", 3L))
                .andExpect(status().isNotFound());
    }
}

What Is Next?

We have now learned how we can write integration tests for a REST API methods that used to read and delete information from a database. This tutorial has taught us two things:

  • We know how to verify the content type of the response body and its character set.
  • We know how we can write assertions against the response body.

In the next part of this tutorial we learn to write integration tests for REST API methods that are used to add new todo entries to the database and update the information of existing todo entries. 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.
12 comments… add one
  • Jari Timonen Dec 7, 2012 @ 19:04

    What value Spring brings to Rest versus Jersey/apache cxf? Seems that Spring MVC is not JSR-311.

    • Petri Dec 7, 2012 @ 20:13

      Jari,

      you right when you say that the REST support of Spring MVC does not implement the JSR-311. It is basically an extension to the "normal" Spring MVC.

      Some say that it does not add any value when compared to Jersey, and if you don't use Spring at all, they are right. However, if your application uses Spring, it might or might not make sense to use some other framework for REST support. I think that this selection is often based on personal preferences.

      I personally like the REST support of Spring MVC and since the applications I work with are using Spring anyway, it kind of makes sense to use it. Of course this does not mean that I think that Jersey or other implementations of the JSR-311 are "bad". ;)

      By the way, if you are looking for another way to write tests for REST APIs, you should check out a library called REST Assured.

  • Rasheed Dec 25, 2012 @ 18:19

    Hi Petri,

    Merry Christmas!

    I am currently trying to setup integration tests for Spring REST controllers. Thanks for whole series of articles they are very informative.

    I couldn't understand one thing that why haven't you mocked your services in integration tests? Any reasons for that? Can you please explain?

    Thanks,
    Rasheed

    • Petri Dec 25, 2012 @ 19:53

      Hi Rasheed,

      Merry Christmas!

      If I had replaced my services with mocks, my tests would tested only the web layer of the todo application. Even though I think that these tests would have been valuable as well, my goals for my this tutorial were a bit different. I simply wanted to demonstrate that writing end-to-end tests (tests that test all layers of an application) to Spring MVC applications can be easy and straightforward.

  • Rasheed Dec 28, 2012 @ 1:44

    Makes sense Petri.

    Would you please drop me an email at the given address above? I would like to discuss some stuff.

    • Petri Dec 28, 2012 @ 10:01

      Sure. Lets continue this discussion by using email.

  • Kavitha Apr 1, 2016 @ 18:39

    Hi,

    How did you initialize mockMvc. I am getting NullPointerException at mockMvc.perform.

    Thanks
    KAvitha

  • Binyam Apr 11, 2016 @ 7:36

    Thank you, your article is very helpful.!
    The delete method on the mockMVc is not recognized
    (mockMvc.perform(delete("/api/todo/{id}", 1L))) ,
    Would you please tell me how to enable and use delete on mockMVC

    • Petri Apr 11, 2016 @ 19:35

      Hi,

      When you say that the delete() method is not recognized, do you mean that your code doesn't compile? If so, have you added the Spring Test dependency into your pom.xml or build.gradle file?

  • Kondu Jun 26, 2019 @ 7:19

    Hi Petri,
    My Service layer has @Transactional attribute and I'm trying to mock the database as done in the post using an xml. When I run the test, it tries to create the session which I don't want to do as I'm using this xml as database. Please help me with that.

  • Cone May 5, 2020 @ 19:32

    Hi,
    I'm new to testing and would like to ask a question about the article. As far as I know (maybe I'm wrong) integration tests work with the real thing(e.g real service) and the real database, so it's not clear to me why we have the mocking here if we do integration testing?
    That was the first question, and second, is it okay to first add a some test item to the test data base when testing the delete method and then delete it, to avoid problems with the relations in the base? Anyone leave a tip, thanks!

Leave a Reply