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
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 theProductController
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 theProductController
that is expected to return a JSON array of strings. ThemockMvc.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 theProductController
. This endpoint is expected to return a JSON objectmap
of strings. Similar to the previous test, it usesmockMvc.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 keys1
,2
, and3
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 theProductController
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 ofProduct
objects usingObjectMapper
. - It then performs assertions on the size of the list and specific attributes of the products in the list.
- This test method sends a GET request to the
- 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.
- This method is similar to the first one but performs an HTTP GET request to the
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
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.
You can download the full source code of this example here: Testing JSON in Spring Boot with MockMvc and JsonPath