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.
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:
1import org.hibernate.annotations.Type;
2import org.joda.time.DateTime;
3
4import javax.persistence.*;
5
6@Entity
7@Table(name="todos")
8public class Todo {
9
10 @Id
11 @GeneratedValue(strategy = GenerationType.AUTO)
12 private Long id;
13
14 @Column(name = "creation_time", nullable = false)
15 @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
16 private DateTime creationTime;
17
18 @Column(name = "description", nullable = true, length = 500)
19 private String description;
20
21 @Column(name = "modification_time", nullable = false)
22 @Type(type="org.jadira.usertype.dateandtime.joda.PersistentDateTime")
23 private DateTime modificationTime;
24
25 @Column(name = "title", nullable = false, length = 100)
26 private String title;
27
28 @Version
29 private long version;
30
31 public Todo() {
32
33 }
34
35 //Getters and other methods
36}
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:
1public interface TodoService {
2
3 public Todo deleteById(Long id) throws TodoNotFoundException;
4
5 public List<Todo> findAll();
6
7 public Todo findById(Long id) throws TodoNotFoundException;
8}
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:
1public class TodoDTO {
2
3 private Long id;
4 private String description;
5 private String title;
6
7 public TodoDTO() {
8
9 }
10
11 //Getters and setters
12}
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:
1import org.springframework.http.HttpStatus;
2import org.springframework.stereotype.Controller;
3import org.springframework.web.bind.annotation.*;
4
5import javax.annotation.Resource;
6
7@Controller
8public class TodoController {
9
10 @Resource
11 private TodoService service;
12
13 @RequestMapping(value = "/api/todo/{id}", method = RequestMethod.DELETE)
14 @ResponseBody
15 public TodoDTO deleteById(@PathVariable("id") Long id) throws TodoNotFoundException {
16 Todo deleted = service.deleteById(id);
17 return createDTO(deleted);
18 }
19
20 @RequestMapping(value = "/api/todo", method = RequestMethod.GET)
21 @ResponseBody
22 public List<TodoDTO> findAll() {
23 List<Todo> models = service.findAll();
24 return createDTOs(models);
25 }
26
27 private List<TodoDTO> createDTOs(List<Todo> models) {
28 List<TodoDTO> dtos = new ArrayList<TodoDTO>();
29
30 for (Todo model: models) {
31 dtos.add(createDTO(model));
32 }
33
34 return dtos;
35 }
36
37 @RequestMapping(value = "/api/todo/{id}", method = RequestMethod.GET)
38 @ResponseBody
39 public TodoDTO findById(@PathVariable("id") Long id) throws TodoNotFoundException {
40 Todo found = service.findById(id);
41 return createDTO(found);
42 }
43
44 private TodoDTO createDTO(Todo model) {
45 TodoDTO dto = new TodoDTO();
46
47 dto.setId(model.getId());
48 dto.setDescription(model.getDescription());
49 dto.setTitle(model.getTitle());
50
51 return dto;
52 }
53
54 @ExceptionHandler(TodoNotFoundException.class)
55 @ResponseStatus(HttpStatus.NOT_FOUND)
56 public void handleTodoNotFoundException(TodoNotFoundException ex) {
57 }
58}
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:
1import org.springframework.http.MediaType;
2import java.nio.charset.Charset;
3
4public class IntegrationTestUtil {
5
6 public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8"));
7
8}
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:
1<dataset>
2 <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"/>
3 <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"/>
4</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:
- Use the @ExceptedDatabase annotation to verify that no changes are made to the database.
- Perform a GET request to url ‘/api/todo’.
- Verify that the HTTP status code is 200.
- Verify that the the content type of the response is ‘application/json’ and its character set is ‘UTF8’
- Verify that the information of the found todo entries is returned.
The source code of our integration test looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener;
2import com.github.springtestdbunit.annotation.DatabaseSetup;
3import com.github.springtestdbunit.annotation.ExpectedDatabase;
4import org.junit.Test;
5import org.junit.runner.RunWith;
6import org.springframework.test.context.ContextConfiguration;
7import org.springframework.test.context.TestExecutionListeners;
8import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
9import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
10import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
11import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
12import org.springframework.test.web.server.MockMvc;
13import org.springframework.test.web.server.samples.context.WebContextLoader;
14
15import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
16import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
17import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
18
19@RunWith(SpringJUnit4ClassRunner.class)
20@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
21@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
22 DirtiesContextTestExecutionListener.class,
23 TransactionalTestExecutionListener.class,
24 DbUnitTestExecutionListener.class })
25@DatabaseSetup("toDoData.xml")
26public class ITTodoControllerTest {
27
28 //Add web application context here
29
30 private MockMvc mockMvc;
31
32 //Add setUp() method here
33
34 @Test
35 @ExpectedDatabase("toDoData.xml")
36 public void findAll() throws Exception {
37 mockMvc.perform(get("/api/todo"))
38 .andExpect(status().isOk())
39 .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
40 .andExpect(content().string("[{\"id\":1,\"description\":\"Lorem ipsum\",\"title\":\"Foo\"},{\"id\":2,\"description\":\"Lorem ipsum\",\"title\":\"Bar\"}]"));
41 }
42}
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:
- Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
- Perform a GET request to url ‘/api/todo/1’.
- Verify that the HTTP status code is 200.
- Verify that the the content type of the response is ‘application/json’ and its character set is ‘UTF8’
- Verify that the information of the correct todo entry is returned
The source code of the first integration test looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener;
2import com.github.springtestdbunit.annotation.DatabaseSetup;
3import com.github.springtestdbunit.annotation.ExpectedDatabase;
4import org.junit.Test;
5import org.junit.runner.RunWith;
6import org.springframework.test.context.ContextConfiguration;
7import org.springframework.test.context.TestExecutionListeners;
8import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
9import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
10import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
11import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
12import org.springframework.test.web.server.MockMvc;
13import org.springframework.test.web.server.samples.context.WebContextLoader;
14
15import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
16import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
17import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
18
19@RunWith(SpringJUnit4ClassRunner.class)
20@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
21@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
22 DirtiesContextTestExecutionListener.class,
23 TransactionalTestExecutionListener.class,
24 DbUnitTestExecutionListener.class })
25@DatabaseSetup("toDoData.xml")
26public class ITTodoControllerTest {
27
28 //Add web application context here
29
30 private MockMvc mockMvc;
31
32 //Add setUp() method here
33
34 @Test
35 @ExpectedDatabase("toDoData.xml")
36 public void findById() throws Exception {
37 mockMvc.perform(get("/api/todo/{id}", 1L))
38 .andExpect(status().isOk())
39 .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
40 .andExpect(content().string("{\"id\":1,\"description\":\"Lorem ipsum\",\"title\":\"Foo\"}"));
41 }
42}
Get Todo Entry When Todo Entry Is Not Found
We can write the second integration test by following these steps:
- Use the @ExpectedDatabase annotation to verify that no changes are made to the database.
- Perform a GET request to url ‘/api/todo/3’.
- Verify that the returned HTTP status code is 404.
The source code of the second integration tests looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener;
2import com.github.springtestdbunit.annotation.DatabaseSetup;
3import com.github.springtestdbunit.annotation.ExpectedDatabase;
4import org.junit.Test;
5import org.junit.runner.RunWith;
6import org.springframework.test.context.ContextConfiguration;
7import org.springframework.test.context.TestExecutionListeners;
8import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
9import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
10import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
11import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
12import org.springframework.test.web.server.MockMvc;
13import org.springframework.test.web.server.samples.context.WebContextLoader;
14
15import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
16import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
17
18@RunWith(SpringJUnit4ClassRunner.class)
19@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
20@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
21 DirtiesContextTestExecutionListener.class,
22 TransactionalTestExecutionListener.class,
23 DbUnitTestExecutionListener.class })
24@DatabaseSetup("toDoData.xml")
25public class ITTodoControllerTest {
26
27 //Add web application context here
28
29 private MockMvc mockMvc;
30
31 //Add setUp() method here
32
33 @Test
34 @ExpectedDatabase("toDoData.xml")
35 public void findByIdWhenTodoIsNotFound() throws Exception {
36 mockMvc.perform(get("/api/todo/{id}", 3L))
37 .andExpect(status().isNotFound());
38 }
39}
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:
- Use the @ExpectedDatabase annotation to ensure that the todo entry is deleted.
- Perform a DELETE request to url ‘/api/todo/1’.
- Verify that the return HTTP status code is 200.
- Verify that the content type of the response is ‘application/json’ and its character set is ‘UTF8’.
- Verify that the information of the deleted todo entry is returned.
The source code of the first integration test looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener;
2import com.github.springtestdbunit.annotation.DatabaseSetup;
3import com.github.springtestdbunit.annotation.ExpectedDatabase;
4import org.junit.Test;
5import org.junit.runner.RunWith;
6import org.springframework.test.context.ContextConfiguration;
7import org.springframework.test.context.TestExecutionListeners;
8import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
9import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
10import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
11import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
12import org.springframework.test.web.server.MockMvc;
13import org.springframework.test.web.server.samples.context.WebContextLoader;
14
15import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
16import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
17import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
18
19@RunWith(SpringJUnit4ClassRunner.class)
20@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
21@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
22 DirtiesContextTestExecutionListener.class,
23 TransactionalTestExecutionListener.class,
24 DbUnitTestExecutionListener.class})
25@DatabaseSetup("toDoData.xml")
26public class ITTodoControllerTest {
27
28 //Add web application context here
29
30 private MockMvc mockMvc;
31
32 //Add setUp() method here
33
34 @Test
35 @ExpectedDatabase("toDoData-delete-expected.xml")
36 public void deleteById() throws Exception {
37 mockMvc.perform(delete("/api/todo/{id}", 1L))
38 .andExpect(status().isOk())
39 .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
40 .andExpect(content().string("{\"id\":1,\"description\":\"Lorem ipsum\",\"title\":\"Foo\"}"));
41 }
42}
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:
1<dataset>
2 <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"/>
3</dataset>
Delete Todo Entry When Todo Entry Is Not Found
We can write the second integration test by following these steps:
- Use the @ExpectedDatabase annotation to ensure that no changes are made to the database.
- Perform a DELETE request to url ‘/api/todo/3’.
- Verify that the returned HTTP status code is 404.
The source code of the second integration test looks as follows:
1import com.github.springtestdbunit.DbUnitTestExecutionListener;
2import com.github.springtestdbunit.annotation.DatabaseSetup;
3import com.github.springtestdbunit.annotation.ExpectedDatabase;
4import org.junit.Test;
5import org.junit.runner.RunWith;
6import org.springframework.test.context.ContextConfiguration;
7import org.springframework.test.context.TestExecutionListeners;
8import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
9import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
10import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
11import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
12import org.springframework.test.web.server.MockMvc;
13import org.springframework.test.web.server.samples.context.WebContextLoader;
14
15import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
16import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
17
18@RunWith(SpringJUnit4ClassRunner.class)
19@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
20@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
21 DirtiesContextTestExecutionListener.class,
22 TransactionalTestExecutionListener.class,
23 DbUnitTestExecutionListener.class})
24@DatabaseSetup("toDoData.xml")
25public class ITTodoControllerTest {
26
27 //Add web application context here
28
29 private MockMvc mockMvc;
30
31 //Add setUp() method here
32
33 @Test
34 @ExpectedDatabase("toDoData.xml")
35 public void deleteByIdWhenTodoIsNotFound() throws Exception {
36 mockMvc.perform(delete("/api/todo/{id}", 3L))
37 .andExpect(status().isNotFound());
38 }
39}
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.