The previous parts of my Spring MVC Test tutorial have described how we can write integration tests for a REST API. Although the techniques described in those blog posts are useful, the problem is that our assertions were not very elegant. Our assertions were basically ensuring that the body of the HTTP response contained the “right” strings.
This approach has two problems:
- It is not very readable, especially if the returned JSON is large. Since our tests should act as a documentation for our code, this is a huge problem.
- It is very hard to write tests which ensure that the ordering of collections is correct without sacrificing readability.
Luckily for us, there is a better way to do this. JsonPath is to JSON what XPath is to XML. It provides an easy and readable way to extract parts of a JSON document. This blog post describes how we can write assertions by using the Spring MVC Test and the Java implementation of JsonPath.
These blog posts describe the structure of our example application and teaches us to write integration tests to it without using the JsonPath library (This blog post skips the details described in these blog posts):
Let’s get started and find out how we can get the required dependencies with Maven.
Getting Required Dependencies with Maven
We can get the required dependencies with Maven by following these steps:
- Declare the Hamcrest dependency (version 1.3) in the pom.xml file.
- Declare the JUnit dependency (version 4.11) in the pom.xml file and exclude the hamcrest dependencies.
- Declare the Spring Test (version 3.2.2.RELEASE) dependency in the pom.xml file.
- Add JsonPath dependencies (version 0.8.1) to the pom.xml file.
The relevant dependency declarations looks as follows:
1<dependency>
2 <groupId>org.hamcrest</groupId>
3 <artifactId>hamcrest-all</artifactId>
4 <version>1.3</version>
5 <scope>test</scope>
6</dependency>
7<dependency>
8 <groupId>junit</groupId>
9 <artifactId>junit</artifactId>
10 <version>4.11</version>
11 <scope>test</scope>
12 <exclusions>
13 <exclusion>
14 <artifactId>hamcrest-core</artifactId>
15 <groupId>org.hamcrest</groupId>
16 </exclusion>
17 </exclusions>
18</dependency>
19<dependency>
20 <groupId>org.springframework</groupId>
21 <artifactId>spring-test</artifactId>
22 <version>3.2.2.RELEASE</version>
23 <scope>test</scope>
24</dependency>
25<dependency>
26 <groupId>com.jayway.jsonpath</groupId>
27 <artifactId>json-path</artifactId>
28 <version>0.8.1</version>
29 <scope>test</scope>
30</dependency>
31<dependency>
32 <groupId>com.jayway.jsonpath</groupId>
33 <artifactId>json-path-assert</artifactId>
34 <version>0.8.1</version>
35 <scope>test</scope>
36</dependency>
Let’s move and find out how we can write assertions by using JsonPath.
Writing Integration Tests
We can write integration tests by executing the tested operation and making assertions against the returned JSON document. We can create a new assertion by following these steps:
- Create a JsonPath expession which fetches the preferred part from the returned JSON document (Get more information about the JsonPath notation).
- Make an assertion against the fetched part by using a Hamcrest matcher.
- Use the jsonPath() method of MockMvcResultMatchers class to verify that the assertion is true and pass the objects created in phases one and two as method parameters.
Enough with theory. Let’s move on and find out how we can write assertions against JSON documents. The following subsections describe how we can write assertions against JSON documents which contains either the information of a single object or the information of multiple objects.
Single Object
This subsection describes how we can ensure that the JSON document which contains the information of a single object is correct. As an example we will write an integration test for a controller method which is used to delete the information of an existing todo entry. After the todo entry has been deleted successfully, its information is returned back to the client. The returned JSON looks as follows:
{
"id":1,
"description":"Lorem ipsum",
"title":"Foo"
}
We can write the 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’. Set the logged in user.
- Verify that the returned HTTP status code is 200.
- Verify that the content type of the response is ‘application/json’ and its character set is ‘UTF-8’.
- Get the id of the deleted todo entry by using the JsonPath expression $.id and verify that the id is 1.
- Get the description of the deleted todo entry by using the JsonPath expression $.description and verify that the description is is “Lorem ipsum”.
- Get the title of the deleted todo entry by using the JsonPath expression $.title and verify that the title is “Foo”.
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.hamcrest.Matchers.*;
16import static org.springframework.test.web.server.samples.context.SecurityRequestPostProcessors.userDetailsService;
17import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
18import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
20import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
21
22@RunWith(SpringJUnit4ClassRunner.class)
23@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
24@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
25 DirtiesContextTestExecutionListener.class,
26 TransactionalTestExecutionListener.class,
27 DbUnitTestExecutionListener.class})
28@DatabaseSetup("toDoData.xml")
29public class ITTodoControllerTest {
30
31 //Add web application context here
32
33 private MockMvc mockMvc;
34
35 //Add setUp() method here
36
37 @Test
38 @ExpectedDatabase("toDoData-delete-expected.xml")
39 public void deleteById() throws Exception {
40 mockMvc.perform(delete("/api/todo/{id}", 1L)
41 .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
42 )
43 .andExpect(status().isOk())
44 .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
45 .andExpect(jsonPath("$.id", is(1)))
46 .andExpect(jsonPath("$.description", is("Lorem ipsum")))
47 .andExpect(jsonPath("$.title", is("Foo")));
48 }
49}
Collection of Objects
This subsection describes how we can write assertions which ensure that the JSON document containing a collection of objects is correct. We will take a look at two different situations:
- The objects are always returned in the same order.
- The objects are returned in a random order.
Let’s continue our journey.
Objects Returned in the Same Order
When the user wants to get all todo entries which are stored in the database, the entries are always returned in the same order. The returned JSON looks as follows:
[
{
"id":1,
"description":"Lorem ipsum",
"title":"Foo"
},
{
"id":2,
"description":"Lorem ipsum",
"title":"Bar"
}
]
We can write our integration test by following these steps:
- Use the @ExpectedDatabase annotation to verify that no changes is made to the database.
- Perform a GET request to url ‘/api/todo’. Set the logged in user.
- Verify that the returned HTTP status code is 200.
- Verify that the content type of the response is ‘application/json’ and its character set ‘UTF-8’.
- Fetch the collection of todo entries by using JsonPath expression $ and ensure that two todo entries are returned.
- Use the JsonPath expressions $[0].id, $[0].description and $[0].title to get the id, description and title of the first todo entry. Verify that its information is correct.
- Use the JsonPath expressions $[1].id, $[1].description and $[1].title to get the id, description and title of the second todo entry. Verify that its information is correct.
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.hamcrest.Matchers.*;
16import static org.springframework.test.web.server.samples.context.SecurityRequestPostProcessors.userDetailsService;
17import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
18import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
20import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
21
22@RunWith(SpringJUnit4ClassRunner.class)
23@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
24@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class,
25 DirtiesContextTestExecutionListener.class,
26 TransactionalTestExecutionListener.class,
27 DbUnitTestExecutionListener.class })
28@DatabaseSetup("toDoData.xml")
29public class ITTodoControllerTest {
30
31 //Add web application context here
32
33 private MockMvc mockMvc;
34
35 //Add setUp() method here
36
37 @Test
38 @ExpectedDatabase("toDoData.xml")
39 public void findAll() throws Exception {
40 mockMvc.perform(get("/api/todo")
41 .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
42 )
43 .andExpect(status().isOk())
44 .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
45 .andExpect(jsonPath("$", hasSize(2)))
46 .andExpect(jsonPath("$[0].id", is(1)))
47 .andExpect(jsonPath("$[0].description", is("Lorem ipsum")))
48 .andExpect(jsonPath("$[0].title", is("Foo")))
49 .andExpect(jsonPath("$[1].id", is(2)))
50 .andExpect(jsonPath("$[1].description", is("Lorem ipsum")))
51 .andExpect(jsonPath("$[1].title", is("Bar")));
52 }
53}
Objects Returned in a Random Order
When a validation of an added or updated todo entry fails, our example application returns the field errors back to the client of our REST API. The problem is that we cannot guarantee the order in which the fields are validated. This means that the field errors are returned in a random order. One JSON document which contains the returned field errors looks as follows:
{
"fieldErrors":[
{
"path":"description",
"message":"The maximum length of the description is 500 characters."
},
{
"path":"title",
"message":"The maximum length of the title is 100 characters."
}
]
}
We can write an write an integration test, which verifies that field errors are returned when a new todo entry which contains invalid information is added, by following these steps:
- Use the @ExpectedDatabase annotation to verify that no changes are made to the database.
- Create the title and description of the todo entry. Ensure that both the title and description are too long.
- Create a new TodoDTO object and set its title and description.
- Perform a POST request to the url ‘/api/todo’. Set the content type of the request to ‘application/json’. Set the character set of the request to ‘UTF-8’. Transform the created object into a correct format and send it in the body of the request. Set the logged in user.
- Verify that the content type of the response is ‘application/json’ and its character set is ‘UTF-8’.
- Fetch the field errors by using the JsonPath expression $.fieldErrors and ensure that two field errors are returned.
- Use the JsonPath expression $.fieldErrors[].path* to fetch all available paths. Ensure that field errors about title and description fields are available.
- Use the JsonPath expression $.fieldErrors[].message* to fetch all available error messages. Ensure that error messages concerning title and description fields are 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.hamcrest.Matchers.*;
16import static org.springframework.test.web.server.samples.context.SecurityRequestPostProcessors.userDetailsService;
17import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
18import static org.springframework.test.web.server.result.MockMvcResultMatchers.content;
19import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
20import static org.springframework.test.web.server.result.MockMvcResultMatchers.status;
21
22@RunWith(SpringJUnit4ClassRunner.class)
23@ContextConfiguration(loader = WebContextLoader.class, classes = {ExampleApplicationContext.class})
24@TestExecutionListeners({DependencyInjectionTestExecutionListener.class,
25 DirtiesContextTestExecutionListener.class,
26 TransactionalTestExecutionListener.class,
27 DbUnitTestExecutionListener.class})
28@DatabaseSetup("toDoData.xml")
29public class ITTodoControllerTest {
30
31 //Add web application context here
32
33 private MockMvc mockMvc;
34
35 //Add setUp() method here
36
37 @Test
38 @ExpectedDatabase("toDoData.xml")
39 public void addTodoWhenTitleAndDescriptionAreTooLong() throws Exception {
40 String title = TodoTestUtil.createStringWithLength(101);
41 String description = TodoTestUtil.createStringWithLength(501);
42 TodoDTO added = TodoTestUtil.createDTO(null, description, title);
43
44 mockMvc.perform(post("/api/todo")
45 .contentType(IntegrationTestUtil.APPLICATION_JSON_UTF8)
46 .body(IntegrationTestUtil.convertObjectToJsonBytes(added))
47 .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
48 )
49 .andExpect(status().isBadRequest())
50 .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
51 .andExpect(jsonPath("$.fieldErrors", hasSize(2)))
52 .andExpect(jsonPath("$.fieldErrors[*].path", containsInAnyOrder("title", "description")))
53 .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder(
54 "The maximum length of the description is 500 characters.",
55 "The maximum length of the title is 100 characters."
56 )));
57 }
58}
Summary
We have now written integration tests to a REST API by using Spring MVC Test and JsonPath. This blog post has taught us four things:
- We learned how we can get the required JsonPath dependencies with Maven.
- We learned how we can write assertions against the JSON representation of a single object.
- We learned how we can write assertions against the JSON representation of a collection of objects.
- We learned that writing assertions with JsonPath improves the readability of our tests.
P.S. You can get the example application of this blog post from Github.