Do you want to be a better Java developer? If so, check out 10 Books Every Java Developer Should Read.

Integration Testing of Spring MVC Applications: Write Clean Assertions with JsonPath

Share
Labyrinth on a blackboard

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:

  1. Declare the Hamcrest dependency (version 1.3) in the pom.xml file.
  2. Declare the JUnit dependency (version 4.11) in the pom.xml file and exclude the hamcrest dependencies.
  3. Declare the Spring Test (version 3.2.2.RELEASE) dependency in the pom.xml file.
  4. Add JsonPath dependencies (version 0.8.1) to the pom.xml file.

The relevant dependency declarations looks as follows:

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>3.2.2.RELEASE</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>0.8.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path-assert</artifactId>
    <version>0.8.1</version>
    <scope>test</scope>
</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:

  1. Create a JsonPath expession which fetches the preferred part from the returned JSON document (Get more information about the JsonPath notation).
  2. Make an assertion against the fetched part by using a Hamcrest matcher.
  3. 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:

  1. Use the @ExpectedDatabase annotation to ensure that the todo entry is deleted.
  2. Perform a DELETE request to url ‘/api/todo/1′. Set the logged in user.
  3. Verify that the returned HTTP status code is 200.
  4. Verify that the content type of the response is ‘application/json’ and its character set is ‘UTF-8′.
  5. Get the id of the deleted todo entry by using the JsonPath expression $.id and verify that the id is 1.
  6. Get the description of the deleted todo entry by using the JsonPath expression $.description and verify that the description is is “Lorem ipsum”.
  7. 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:

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.samples.context.SecurityRequestPostProcessors.userDetailsService;
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.servlet.result.MockMvcResultMatchers.jsonPath;
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)
                .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
        )
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$.id", is(1)))
                .andExpect(jsonPath("$.description", is("Lorem ipsum")))
                .andExpect(jsonPath("$.title", is("Foo")));
    }
}

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:

  1. Use the @ExpectedDatabase annotation to verify that no changes is made to the database.
  2. Perform a GET request to url ‘/api/todo’. Set the logged in user.
  3. Verify that the returned HTTP status code is 200.
  4. Verify that the content type of the response is ‘application/json’ and its character set ‘UTF-8′.
  5. Fetch the collection of todo entries by using JsonPath expression $ and ensure that two todo entries are returned.
  6. 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.
  7. 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:

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.samples.context.SecurityRequestPostProcessors.userDetailsService;
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.servlet.result.MockMvcResultMatchers.jsonPath;
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")
                .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
        )
                .andExpect(status().isOk())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$", hasSize(2)))
                .andExpect(jsonPath("$[0].id", is(1)))
                .andExpect(jsonPath("$[0].description", is("Lorem ipsum")))
                .andExpect(jsonPath("$[0].title", is("Foo")))
                .andExpect(jsonPath("$[1].id", is(2)))
                .andExpect(jsonPath("$[1].description", is("Lorem ipsum")))
                .andExpect(jsonPath("$[1].title", is("Bar")));
    }
}

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:

  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. Ensure that both the title and description are too long.
  3. Create a new TodoDTO object and set its title and description.
  4. 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.
  5. Verify that the content type of the response is ‘application/json’ and its character set is ‘UTF-8′.
  6. Fetch the field errors by using the JsonPath expression $.fieldErrors and ensure that two field errors are returned.
  7. Use the JsonPath expression $.fieldErrors[*].path to fetch all available paths. Ensure that field errors about title and description fields are available.
  8. 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:

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.samples.context.SecurityRequestPostProcessors.userDetailsService;
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.servlet.result.MockMvcResultMatchers.jsonPath;
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))
                .with(userDetailsService(IntegrationTestUtil.CORRECT_USERNAME))
        )
                .andExpect(status().isBadRequest())
                .andExpect(content().mimeType(IntegrationTestUtil.APPLICATION_JSON_UTF8))
                .andExpect(jsonPath("$.fieldErrors", hasSize(2)))
                .andExpect(jsonPath("$.fieldErrors[*].path", containsInAnyOrder("title", "description")))
                .andExpect(jsonPath("$.fieldErrors[*].message", containsInAnyOrder(
                        "The maximum length of the description is 500 characters.",
                        "The maximum length of the title is 100 characters."
                )));                
    }
}

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.

The example application of this blog post is available at Github.

P.S. You might want to read the other parts of my Spring MVC Test tutorial.

If you enjoy reading similar content, you should follow me on Twitter:

About the Author

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

About Petri Kainulainen →

2 comments… add one

  • Update: I removed some unrelevant code to make this question a bit more clear – Petri

    This is my Controller Method

    
    @RequestMapping(value="/TradesMngJson.do")
    public @ResponseBody List getFilteredEMFTradesAsJson(@ModelAttribute("model") ModelMap model,
    		HttpServletRequest request, 
    		@RequestParam(required=false,value="source") String paramSource,
    		@RequestParam(required=false,value="status") String paramStatus,
    		@RequestParam(required=false,value="account") String paramAccount,
    		@RequestParam(required=false,value="fromDate") String paramFromDate,
    		@RequestParam(required=false,value="toDate") String paramToDate,
    		@RequestParam(required=false,value="rcvdDate") String paramRcvdDate){
    
    	List tradeList;
    	try{
    		//I removed some code because it wasn't relevant
    
    		tradeList=(List)(List) tradeInService.getFilteredTrades(filterMap);
    		tradeList=(List)(List)tradeInService.filterByDates(
    				(List)(List)tradeList,
    				fromDate,
    				toDate
    		);
    		if(paramSource!=null && paramSource.trim()!=""){ 
    			model.addAttribute("source",paramSource);
    		}
    		
    		//Removed some code again because it wasn't relevant
    
    		if(tradeList.size() == 0){
    			List error = new ArrayList();
    			error.add(new ErrorBean(
    					constants.NODATA_ERROR_CODE, 
    					constants.NODATA_ERROR_MSG_TO_DISPLAY
    			));
    			return (List)(List) error;
    		}else{
    			return tradeList;
    		}
    	} catch(Exception e){
    		logger.error("Exception:", e);
    		List error = new ArrayList();
    		error.add(new ErrorBean(
    				"100", 
    				"No data available, contact to admin"
    		));
    		tradeList = (List)(List) error;
    		return tradeList;
    	}
    }
    
    

    This is the test case that i have written for the Controller Method.

    
    @RunWith(SpringJUnit4ClassRunner.class)
    @WebAppConfiguration
    @ContextConfiguration({
    		"file:src/main/webapp/WEB-INF/spring/appServlet/test-config.xml" 
    })
    public class EMFTradesControllerMock {
    
    	@InjectMocks
    	EMFTradesController emftradescontrollermock;
    
    	@Mock
    	TradeInService tradeInServicemock;
    
    	private MockMvc mockEMF;
    
    	MockHttpServletRequest request = new MockHttpServletRequest();
    	ModelMap model = new ModelMap();
    
    	@Autowired
    	private WebApplicationContext webApplicationContext;
    
    	@Before
    	public void setUp() throws Exception {
    		MockitoAnnotations.initMocks(this);
    		mockEMF = MockMvcBuilders.standaloneSetup(emftradescontrollermock).build();
    	}
    
    	@SuppressWarnings("unchecked")
    	@Test
    	public void test() throws Exception {
    		TradeOverviewBean bean = new TradeOverviewBeanBuilder().aCCT_CD("1")
    				.tPH_TRD_SEQ_NO((long) 62).source("BR").transactionType("BUY")
    				.tRD_REF_NO((long) -100001).cUSIP("121899DJ4").status("N")
    				.tOUCH_COUNT(1).brokerCode("BEAR").executionCurrency("USD")
    				.executionPrice((double) 101)
    				.executionAmount((double) 1020347.22).build();
    		
    		when(tradeInServicemock.getFilteredTrades((Map) Mockito.anyMap()))
    				.thenReturn(Arrays.asList(bean));
    		when(tradeInServicemock.filterByDates(
    				(List) Mockito.anyList(),
    				(Date) Mockito.any(), 
    				(Date) Mockito.any())
    		)
    				.thenReturn(Arrays.asList(bean));
    				
    		mockEMF.perform(get("/TradesMngJson.do"))
    				.andExpect(status().isOk())
    				.andExpect(content().contentType(
    						MediaType.parseMediaType("application/json;charset=UTF-8"
    				)))
    				.andExpect(jsonPath("$.source").value("BR"));
        }
    }
    
    

    When i run the test case i get the error

    java.lang.AssertionError: JSON path$.source expected: but was:

    Just wanted to know where i am going wrong. According to me everything seems to be fine. but the data which i am passing is not returning in the JSON path. I followed all the procedures to test a json data.

    Please suggest me an idea

    Reply
    • It seems that you are expecting that the information which you add to the ModelMap object would be returned back to the client as JSON document. Spring MVC doesn’t work this way.

      When you annotate a controller method with the @ResponseBody annotation, Spring MVC transforms the information returned by this method into a JSON document (or an XML document depending from your configuration) and returns it back to the client.

      If you want to return the information which you add to the ModelMap object as well, you have to create a wrapper class which contains all returned information. In other words, your code could look like this:

      
      //The wrapper class
      public class TradeInfo {
      
      	private String source;
      	
      	private List tradeList;
      	
      	public TradeInfo() {
      	}
      	
      	//Getters and setters are omitted
      }
      
      //Example controller method
      @RequestMapping(value="/TradesMngJson.do")
      public @ResponseBody TradeInfo getFilteredEMFTradesAsJson(
      		HttpServletRequest request, 
      		@RequestParam(required=false,value="source") String paramSource,
      		@RequestParam(required=false,value="status") String paramStatus,
      		@RequestParam(required=false,value="account") String paramAccount,
      		@RequestParam(required=false,value="fromDate") String paramFromDate,
      		@RequestParam(required=false,value="toDate") String paramToDate,
      		@RequestParam(required=false,value="rcvdDate") String paramRcvdDate){
      
      		//Get tradelist
      		List tradeList = ...
      		
      		//Create returned object
      		TradeInfo info = new TradeInfo();
      		info.setSource("source")
      		info.setTradeList(tradeList)
      		
      		//Return the information which is transformed into JSON.
      		return info;
      }
      
      
      Reply

Leave a Comment