Core JavaEnterprise Java

Testing JSON in Spring Boot with MockMvc and JsonPath

Testing is crucial when developing software applications, and when developing RESTful APIs in a Spring Boot application, It’s important to have strong testing methods to ensure everything works as intended. This article will explore using MockMvc and JsonPath to test JSON responses in a Spring Boot application.

1. Importance of Testing JSON Responses in Web Applications.

Testing JSON responses is a fundamental necessity in the development of web applications. The benefits include data validation, incorporating elements of interoperability, security, performance, and user experience. Here is a breakdown of the benefits testing JSON responses brings to the development and maintenance of web applications.

  • Data Integrity and Accuracy: One of the primary reasons for testing JSON responses is to guarantee the integrity and accuracy of the data being exchanged. Web applications often rely on APIs to send and receive JSON payloads. Testing ensures that the data adheres to the expected format, structure, and content.
  • Interoperability: Web applications often interact with multiple services and APIs, each producing or consuming JSON data. Testing JSON responses helps verify that our application can correctly parse external data and that it produces responses compatible with other systems.
  • Front-End Development: In modern web development, the front-end and back-end are decoupled, often communicating through APIs. Front-end developers consume JSON responses to render user interfaces. By testing JSON, we assure front-end developers that the data they expect is consistently delivered.
  • Security: Security is a critical concern in web applications, and testing JSON responses contributes to a more secure system. By validating that sensitive information is not exposed in responses and that security headers are correctly set, we reduce the risk of data breaches and unauthorized access.

2. Setting Up a Spring Boot Project for Testing

Let’s begin by creating a new Spring Boot project using our preferred IDE or Spring Initializr and including the dependencies for testing, such as JUnit and Spring Boot Test. These dependencies provide the foundation for writing and executing tests within our Spring Boot application.

2.1 Project Structure

Fig 1: Project structure used for testing JSON responses in spring boot with MockMvc and JsonPath
Fig 1: Project structure used for testing JSON responses in spring boot with MockMvc and JsonPath

2.2 What is spring-boot-starter-test?

The spring-boot-starter-test is a starter dependency that comes with Spring Boot. It makes testing in Spring Boot applications much easier for developers. It gathers useful testing tools (JUnit, Hamcrest, Mockito, AssertJ, JsonPath, Spring Boot Test) and settings, so we can write and run tests without having to do a lot of complicated setup work.

In our project, add the following snippet to the pom.xml file to include spring-boot-starter-test as a dependency:

<dependencies>
    <!-- Other dependencies -->

    <!-- Spring Boot Starter Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Note: When we create a Spring Boot application using the Spring Boot initializer, the Spring Boot Starter Test dependency is automatically added to our classpath.

3. Understanding MockMvc

MockMvc is a class provided by Spring for testing MVC controllers without actually starting a full HTTP server. It is designed to simulate HTTP requests and responses without actually sending them over the network. This makes it an ideal choice for thoroughly testing the behavior of our controllers without the need for a running server.

3.1 Setting up MockMvc

Two annotations commonly used in the Spring framework for testing are @AutoConfigureMockMvc and @WebMvcTest but they serve different purposes.

3.1.1 @AutoConfigureMockMvc

@AutoConfigureMockMvc is used to automatically configure the MockMvc instance in our test context. This annotation is often used in integration tests where we want to test the entire application context, including the web layer, but without the need to start a real web server.

To set up MockMvc with @AutoConfigureMockMvc in a Spring Boot test, we typically need to follow these steps:

  • Annotate our test class with @SpringBootTest, and @AutoConfigureMockMvc.
  • Inject the MockMvc instance into our test class using @Autowired.
  • Write test methods using the injected MockMvc instance to perform HTTP requests and assert responses.

3.1.2 Testing a Basic JSON Structure

Here’s an example setup used to test a rest controller endpoint that returns a JSON string.

@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void testSimpleEndPoint() throws Exception{
     mockMvc.perform(get("/api/product")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("Real World Java EE Patterns"))
                .andDo(print());
    }

}

In the above code, The .andExpect(jsonPath("$.name").value("Real World Java EE Patterns")) test if the JSON data contains a field name called name with a value of Real World Java EE Patterns.

The controller looks like this:

@RestController
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/product")
    public String product() {
        // Construct a JSON string (for simplicity, using a hardcoded string)
        String jsonString = "{\"name\": \"Real World Java EE Patterns\"}";
        return jsonString;
    }

}

3.1.3 @WebMvcTest

@WebMvcTest is a more specialized annotation. It is used to test our application’s web layer by focusing only on the controllers. When we use @WebMvcTest, Spring Boot will only instantiate the web layer of the application and will not start the full application context. It is designed to be more lightweight and is suitable for testing controllers in isolation.

Here is an example to set up MockMvc with @WebMvcTest:

@WebMvcTest(ProductController.class)
public class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void testSimpleEndPoint() throws Exception{
     mockMvc.perform(get("/api/product")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("Real World Java EE Patterns"))
                .andDo(print());
    }

}

In this example, start by annotating our test class with @WebMvcTest(ProductController.class). The ProductController.class is the controller we want to test, and MockMvc is injected using @Autowired.

If we run our test class using ./mvnw test, the tests will automatically instantiate only the relevant parts of the Spring context for testing the web layer (controllers), set up MockMvc, and execute the specified test methods.

For the rest of this article, we will be using the @WebMvcTest annotation.

4. Handling Maps and Lists

MockMvc and jsonPath make it easier to check if Maps and Lists in JSON responses are correct. These tools are helpful for testing because they offer a simple and efficient way to make sure that the complicated data structures in JSON responses are accurate and match what’s expected.

4.1 Testing Maps and Lists of String

Below is a REST controller that returns a String, a List of Strings, and a Map containing Strings in JSON format.

@RestController
@RequestMapping("/api")
public class ProductController {

    @GetMapping("/product")
    public String product() {
        // Construct a JSON string (for simplicity, using a hardcoded string)
        String jsonString = "{\"name\": \"Real World Java EE Patterns\"}";
        return jsonString;
    }

    @GetMapping("/strings")
    public List<String> returnList() {
        return Arrays.asList("The Book of CSS3", "Core HTML5 Canvas", "Pro JavaScript for Web Apps");
    }

    @GetMapping("/stringmap")
    public Map<String, String> returnMap() {
        Map<String, String> stringMap = new HashMap<>();

        stringMap.put("1", "Pro JavaScript for Web Apps");
        stringMap.put("2", "JavaScript Enlightenment");
        stringMap.put("3", "Real World Java EE Patterns");

        return stringMap;
    }

}

We can use MockMvc and jsonPath to test the JSON response from the controller class above like this:

@WebMvcTest(ProductController.class)
public class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;
    
    // ObjectMapper for converting objects to JSON
    @Autowired
    private ObjectMapper objectMapper;
    
    @Test
    public void testSimpleEndPoint() throws Exception{
     mockMvc.perform(get("/api/product")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("Real World Java EE Patterns"))
                .andDo(print());
    }

    @Test
    public void testListOfStrings() throws Exception {
        mockMvc.perform(get("/api/strings")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(3)))
                .andExpect(jsonPath("$[0]", is("The Book of CSS3")))
                .andExpect(jsonPath("$[1]", is("Core HTML5 Canvas")))
                .andExpect(jsonPath("$[2]", is("Pro JavaScript for Web Apps")))
                .andDo(print());
    }

    @Test
    public void testStringMap() throws Exception {
        mockMvc.perform(get("/api/stringmap")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.1", is("Pro JavaScript for Web Apps")))
                .andExpect(jsonPath("$.2", is("JavaScript Enlightenment")))
                .andExpect(jsonPath("$.3", is("Real World Java EE Patterns")))
                .andDo(print());
    }

}

The above code block represents a set of unit tests for the controller named ProductController. Here’s a breakdown of the key elements of the code:

  • @WebMvcTest(ProductController.class): This annotation is used to indicate that the tests in this class should focus on the ProductController. By using @WebMvcTest, the application context will only contain components relevant to handling the ProductController class.
  • @Autowired private MockMvc mockMvc: The MockMvc instance is injected using the @Autowired annotation.
  • testListOfStrings(): This method is a unit test for the endpoint (/api/strings) in the ProductController that is expected to return a JSON array of strings. The mockMvc.perform() method initiates an HTTP GET request to the /api/strings endpoint with the content type set to JSON.
  • The andExpect() methods are used to assert expectations about the response as follows:
    • status().isOk(): Expects the HTTP status code to be 200 (OK).
    • jsonPath("$", hasSize(3)): Expects that the JSON array at the root level has a size of 3.
    • jsonPath("$[0]", is("The Book of CSS3")), jsonPath("$[1]", is("Core HTML5 Canvas")), jsonPath("$[2]", is("Pro JavaScript for Web Apps")): Expects specific values at the corresponding array indices.
  • The andDo(print()) method is used to print the request and response details to the console for debugging purposes.
  • testStringMap(): This method is another unit test for a different endpoint (/api/stringmap) in the ProductController. This endpoint is expected to return a JSON object map of strings. Similar to the previous test, it uses mockMvc.perform() to send an HTTP GET request.
  • The andExpect() methods validate expectations about the response:
    • status().isOk(): Expects the HTTP status code to be 200 (OK).
    • jsonPath("$.1", is("Pro JavaScript for Web Apps")), jsonPath("$.2", is("JavaScript Enlightenment")), jsonPath("$.3", is("Real World Java EE Patterns")): Expects specific values for the keys 1, 2, and 3 in the JSON object.

4.2 Testing List and Map of Objects

This section shows how to test a Spring Boot controller that returns a List and Map of Product objects using MockMvc for making HTTP requests and JsonPath for parsing and asserting the JSON response.

4.2.1 Controller and Product Model

First, define a Product model with the following attributes – id, name, and price:


public class Product {

    private int id;
    private String name;
    private Double price;

    //constructors, getters and setters

    @Override
    public String toString() {
        return "Product{" + "id=" + id + ", name=" + name + ", price=" + price + '}';
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 79 * hash + Objects.hashCode(this.id);
        hash = 79 * hash + Objects.hashCode(this.name);
        hash = 79 * hash + Objects.hashCode(this.price);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Product other = (Product) obj;
        if (!Objects.equals(this.name, other.name)) {
            return false;
        }
        if (!Objects.equals(this.id, other.id)) {
            return false;
        }
        return Objects.equals(this.price, other.price);
    }  
    
}

Below is the code snippet from the ProductController class that returns a List and Map of Product object in JSON:

@RestController
@RequestMapping("/api")
public class ProductController {


    @GetMapping("/products")
    public List<Product> returnProductList() {

        // Creating a List of Product entities
        List<Product> productList = new ArrayList<>();

        // Adding products to the List
        productList.add(new Product(1, "The Book of CSS3", 50.00));
        productList.add(new Product(2, "JavaScript and JQuery", 70.00));
        productList.add(new Product(3, "Core HTML5 Canvas", 90.00));

        return productList;
    }

        @GetMapping("/productmap")
    public Map<Integer, Product> returnProductMap() {

        // Creating a Map of Product entities with product IDs as keys
        Map<Integer, Product> productMap = new HashMap<>();

        // Adding products to the map
        Product product1 = new Product(1, "Laptop", 999.99);
        Product product2 = new Product(2, "Smartphone", 599.99);

        productMap.put(product1.getId(), product1);
        productMap.put(product2.getId(), product2);

        return productMap;
    }
}

Here’s the test class for the above controller:

@WebMvcTest(ProductController.class)
public class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    // ObjectMapper for converting objects to JSON
    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void testProductList() throws Exception {

         // perform a GET request to "/api/products" and retrieve the response
        MvcResult mvcResult = mockMvc.perform(get("/api/products")
                .accept(MediaType.APPLICATION_JSON)).andReturn();

        objectMapper = new ObjectMapper();
        
        // Convert the JSON response to a List
        String responseContent = mvcResult.getResponse().getContentAsString();
        List productList = objectMapper.readValue(responseContent, new TypeReference<List>() {});

        // Assert the size of the list
        assertThat(productList.size()).isEqualTo(3);

         //perform additional assertions on the JSON response
        mockMvc.perform(get("/api/products")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$", hasSize(3)))
                .andExpect(jsonPath("$[0].name", is("The Book of CSS3")))
                .andExpect(jsonPath("$[0].price", is(50.00)))
                .andExpect(jsonPath("$[1].name", is("JavaScript and JQuery")))
                .andExpect(jsonPath("$[1].price", is(70.00)))
                .andExpect(jsonPath("$[2].name", is("Core HTML5 Canvas")))
                .andExpect(jsonPath("$[2].price", is(90.00)))
                .andDo(print());

    }

    @Test
    public void testProductMap() throws Exception {
        
        // perform a GET request to "/api/productmap" and retrieve the response
        MvcResult result = mockMvc.perform(get("/api/productmap"))
                .andExpect(status().isOk())
                .andReturn();

        //Convert the JSON response to a Map<String, Product>
        String responseContent = result.getResponse().getContentAsString();
        Map<String, Product> productMap = objectMapper.readValue(responseContent, new TypeReference<Map<String, Product>>() {});

        // Assert the size of the map
        assertThat(productMap.size()).isEqualTo(2);
        
        //perform additional assertions on the JSON response, treating it as a map
        mockMvc.perform(get("/api/productmap")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.1.name", is("Laptop")))
                .andExpect(jsonPath("$.1.price", is(999.99)))
                .andExpect(jsonPath("$.2.name", is("Smartphone")))
                .andExpect(jsonPath("$.2.price", is(599.99)))
                .andDo(print());
    }

}

In the above tests:

  • @WebMvcTest(ProductController.class) is used to focus on testing the ProductController without starting the whole Spring application context.
  • MockMvc is auto-wired to perform HTTP requests and validate responses.
  • ObjectMapper is auto-wired to convert objects to JSON for request bodies.
  • The testGetProductList method:
    • This test method sends a GET request to the /api/products endpoint, retrieves the response, and converts it to a list of Product objects using ObjectMapper.
    • It then performs assertions on the size of the list and specific attributes of the products in the list.
  • The testGetProductMap method:
    • This method is similar to the first one but performs an HTTP GET request to the /api/productmap endpoint, expecting a JSON response that represents a map of products.
    • It then performs assertions on the size of the map and specific attributes of the products in the map.

4.3 Run the Tests

Finally, we can run the test class and execute the test method using the following command on a Terminal:

./mvnw test

4.4 Console Output

Fig 2: Console output on testing JSON responses in spring boot with MockMvc and JsonPath
Fig 2: Console output on testing JSON responses in spring boot with MockMvc and JsonPath

5. Conclusion

In this article, we’ve looked at different examples of testing scenarios where our Spring Boot application deals with JSON responses. By using tools like MockMvc and JsonPath, these tests help make our application stronger and more reliable.

Testing JSON in Spring Boot with MockMvc and JsonPath helps us create tests that work well and are easy to understand for our RESTful APIs. By using the testing tools that Spring provides, we can make sure our controllers generate the right JSON responses. This way, we can find and fix problems early on in the development process.

In conclusion, testing a Spring Boot controller ensures that our endpoints produce the correct responses. These tests provide confidence in the behavior of our controllers, and they are crucial for maintaining the reliability of our Spring Boot application.

6. Download the Source Code

This was a tutorial on Testing JSON in Spring Boot with MockMvc and JsonPath.

Download
You can download the full source code of this example here: Testing JSON in Spring Boot with MockMvc and JsonPath

Omozegie Aziegbe

Omos holds a Master degree in Information Engineering with Network Management from the Robert Gordon University, Aberdeen. Omos is currently a freelance web/application developer who is currently focused on developing Java enterprise applications with the Jakarta EE framework.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button