Testing JSON in Spring Boot
This article shows how to use MockMvc
and JsonPath to test JSON in Spring Boot.
Table of Contents:
- 1. Spring Boot Test Dependencies
- 2. Testing JSON Simple Structure
- 3. Testing a List
- 4. Testing a Map
- 5. Testing JSON in Spring Boot
- 6. Download Source Code
- 7. References
Note
Read more about JsonPath.
P.S. Tested with Spring Boot 3.1.2
1. Spring Boot Test Dependencies
The spring-boot-starter-test
has all the dependencies (jsonpath
, assertj
, hamcrest
, jUnit
, mockito
) we need to test JSON in Spring Boot.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- optional, only if we want test Java 8 date time APIs -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
$ mvn dependency:tree
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:3.1.2:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:3.1.2:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:3.1.2:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.8.0:test
[INFO] | | \- org.slf4j:slf4j-api:jar:2.0.7:compile
[INFO] | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:4.0.0:test
[INFO] | | \- jakarta.activation:jakarta.activation-api:jar:2.1.2:test
[INFO] | +- net.minidev:json-smart:jar:2.4.11:test
[INFO] | | \- net.minidev:accessors-smart:jar:2.4.11:test
[INFO] | | \- org.ow2.asm:asm:jar:9.3:test
[INFO] | +- org.assertj:assertj-core:jar:3.24.2:test
[INFO] | | \- net.bytebuddy:byte-buddy:jar:1.14.5:test
[INFO] | +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.9.3:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-api:jar:5.9.3:test
[INFO] | | | +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] | | | +- org.junit.platform:junit-platform-commons:jar:1.9.3:test
[INFO] | | | \- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-params:jar:5.9.3:test
[INFO] | | \- org.junit.jupiter:junit-jupiter-engine:jar:5.9.3:test
[INFO] | | \- org.junit.platform:junit-platform-engine:jar:1.9.3:test
[INFO] | +- org.mockito:mockito-core:jar:5.3.1:test
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.14.5:test
[INFO] | | \- org.objenesis:objenesis:jar:3.3:test
[INFO] | +- org.mockito:mockito-junit-jupiter:jar:5.3.1:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.1:test
[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-core:jar:6.0.11:compile
[INFO] | | \- org.springframework:spring-jcl:jar:6.0.11:compile
[INFO] | +- org.springframework:spring-test:jar:6.0.11:test
[INFO] | \- org.xmlunit:xmlunit-core:jar:2.9.1:test
2. Testing JSON Simple Structure
Below is a rest controller endpoint that returns JSON data.
curl /endpoint
{"name" : "hello world"}
In unit tests, we can use JSONPath expressions to navigate through JSON data and extract values for testing.
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// too heavy to load entire spring context, uses @WebMvcTest
//@SpringBootTest
//@AutoConfigureMockMvc
@WebMvcTest(MyController.class)
public class MyControllerTest {
@Autowired
private MockMvc mvc;
@Test
public void testHello() throws Exception {
mvc.perform(get("/endpoint")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("hello world"));
}
}
The .andExpect(jsonPath("$.fieldname").value("field value"))
test if the JSON data contains field name name
with a value of hello world
.
3. Testing a List
A List
of strings in JSON format.
[
"Java",
"React",
"JavaScript"
]
And we can use the same jsonPath
to test the JSON List
.
@Test
public void testList() throws Exception {
mvc.perform(get("/list")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
// $ refer to root element
.andExpect(jsonPath("$", hasSize(3)))
// $[0] refer to first element of the list
.andExpect(jsonPath("$[0]").value("Java"))
.andExpect(jsonPath("$[1]").value("React"))
.andExpect(jsonPath("$[2]").value("JavaScript"))
// normally list order is not fix, better use hasItem
// to test if it contains a specific value
.andExpect(jsonPath("$", hasItem("React")));
}
Note
$
: Refers to the root element.$[0]
: Refers to the first element of the list.
We can use hamcrest.Matchers.hasItem
to test if the list contains a specific value.
4. Testing a Map
A Map
in JSON format.
{
"key1": "a",
"key2": "b",
"key3": "c"
}
And we can use the same jsonPath
to test the Map
.
@Test
public void testMap() throws Exception {
mvc.perform(get("/map")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.key1").value("a"))
.andExpect(jsonPath("$.key2").value("b"))
.andExpect(jsonPath("$.key3").value("c"));
// Deserialize and assert to test the map size, is there a better way?
MvcResult result = mvc.perform(get("/map")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
// convert JSON to Map object
String content = result.getResponse().getContentAsString();
Map<String, Object> resultMap =
new ObjectMapper().readValue(content, new TypeReference<>() {
});
assertEquals(3, resultMap.size());
}
5. Testing JSON in Spring Boot
Below is a complete example of how we can use jsonPath
to test the JSON returned from REST controller endpoints.
5.1 Spring Boot REST Controller returns JSON
Below are a few Spring Boot REST controller endpoints that return JSON in various formats like String
, List
, Map
, and a list of objects (a little complicated JSON format).
package com.mkyong;
import com.mkyong.model.Author;
import com.mkyong.model.Book;
import com.mkyong.model.SimpleBook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@RestController
public class WebController {
private static final Logger logger =
LoggerFactory.getLogger(WebController.class);
@GetMapping("/")
public SimpleBook main() {
return new SimpleBook("Hello World");
}
@GetMapping("/book")
public Book returnBook() {
Author obj1 = new Author(1L, "Raoul-Gabriel Urma", "111-1111111");
Author obj2 = new Author(2L, "Mario Fusco", "222-2222222");
Author obj3 = new Author(3L, "Alan Mycroft", "333-3333333");
Book book = new Book();
book.setId(1L);
book.setTitle("Modern Java in Action");
book.setAuthors(List.of(obj1, obj2, obj3));
book.setTags(List.of("Java", "Java 8"));
book.setPublishedDate(LocalDate.of(2018, 11, 15));
book.setMeta(Map.of("isbn-10", "1617293563", "isbn-13", "978-1617293566"));
return book;
}
@GetMapping("/list")
public List<String> returnList() {
return List.of("Java", "React", "JavaScript");
}
@GetMapping("/map")
public Map<String, String> returnMap() {
return Map.of("key1", "a", "key2", "b", "key3", "c");
}
}
Below are some objects that convert to JSON for testing.
package com.mkyong.model;
public class SimpleBook {
private String title;
public SimpleBook(String title) {
this.title = title;
}
//getters and setters
}
package com.mkyong.model;
import java.util.Objects;
public class Author {
private long id;
private String name;
private String phoneNo;
// test array or list objects need equals and hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Author author = (Author) o;
return id == author.id && Objects.equals(name, author.name)
&& Objects.equals(phoneNo, author.phoneNo);
}
@Override
public int hashCode() {
return Objects.hash(id, name, phoneNo);
}
//constructor, getters ad setters
}
package com.mkyong.model;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
public class Book {
private long id;
private String title;
private List<String> tags;
private List<Author> authors;
private LocalDate publishedDate;
private Map<String, String> meta;
//contructors, getters and setters, toString and etc.
}
5.1 Testing JSON Data
Below is a complete test for the above WebController
.
package com.mkyong;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.mkyong.model.Author;
import com.mkyong.model.Book;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
// too heavy to load entire spring context, uses @WebMvcTest
//@SpringBootTest
//@AutoConfigureMockMvc
@WebMvcTest(WebController.class)
public class WebControllerTest {
@Autowired
private MockMvc mvc;
@Test
public void testHello() throws Exception {
mvc.perform(get("/")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
// has field name "$title" with a value of "Hello World"
.andExpect(jsonPath("$.title").value("Hello World"));
}
/**
* {
* "id" : 1,
* "title" : "Modern Java in Action",
* "tags" : [ "Java", "Java 8" ],
* "authors" : [ {
* "id" : 1,
* "name" : "Raoul-Gabriel Urma",
* "phoneNo" : "111-1111111"
* }, {
* "id" : 2,
* "name" : "Mario Fusco",
* "phoneNo" : "222-2222222"
* }, {
* "id" : 3,
* "name" : "Alan Mycroft",
* "phoneNo" : "333-3333333"
* } ],
* "publishedDate" : "2018-11-15",
* "meta" : {
* "isbn-10" : "1617293563",
* "isbn-13" : "978-1617293566"
* }
* }
*/
@Test
public void testBook() throws Exception {
mvc.perform(MockMvcRequestBuilders.get("/book")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.id").isNumber())
.andExpect(jsonPath("$.title").exists())
.andExpect(jsonPath("$.title", is("Modern Java in Action")))
.andExpect(jsonPath("$.bookName").doesNotExist())
.andExpect(jsonPath("$.bookName").doesNotExist())
.andExpect(jsonPath("$.tags").isArray())
.andExpect(jsonPath("$.tags", hasSize(2)))
.andExpect(jsonPath("$.tags", hasItem("Java"))) // order not fix, check with contains
.andExpect(jsonPath("$.tags", hasItem("Java 8")))
.andExpect(jsonPath("$.publishedDate", is(LocalDate.of(2018, 11, 15).toString())))
.andExpect(jsonPath("$.authors", hasSize(3)))
.andExpect(jsonPath("$.meta.isbn-10", is("1617293563")))
.andExpect(jsonPath("$.meta.isbn-13", is("978-1617293566")))
// better convert to list of objects and test it, see below testBookAuthor
.andExpect(jsonPath("$.authors[*].id", hasItem(1)))
.andExpect(jsonPath("$.authors[*].id", containsInAnyOrder(3, 1, 2)))
.andExpect(jsonPath("$.authors[*].name", hasItem("Raoul-Gabriel Urma")))
.andExpect(jsonPath("$.authors[*].phoneNo", hasItem("111-1111111")));
/*.andExpect(jsonPath("$.authors[0].id").value(1)) // first author of the book
.andExpect(jsonPath("$.authors[0].name").value("Raoul-Gabriel Urma"))
.andExpect(jsonPath("$.authors[0].phoneNo").value("111-1111111"))
.andExpect(jsonPath("$.authors[1].id").value(2)) // second author of the book
.andExpect(jsonPath("$.authors[2].name").value("Mario Fusco"))
.andExpect(jsonPath("$.authors[3].phoneNo").value("222-2222222")
);*/
}
// Better convert to list of objects and test it
@Test
public void testBookAuthor() throws Exception {
MvcResult mvcResult = mvc.perform(get("/book")
.accept(MediaType.APPLICATION_JSON)).andReturn();
ObjectMapper mapper = new ObjectMapper();
// supports Java 8 date time
mapper.registerModule(new JavaTimeModule());
Book book = mapper.readValue(mvcResult.getResponse().getContentAsString(), Book.class);
List<Author> authors = book.getAuthors();
Author obj1 = new Author(1L, "Raoul-Gabriel Urma", "111-1111111");
Author obj2 = new Author(2L, "Mario Fusco", "222-2222222");
Author obj3 = new Author(3L, "Alan Mycroft", "333-3333333");
assertThat(authors.size(), is(3));
assertThat(authors, hasItem(obj1));
assertThat(authors, hasItem(obj2));
assertThat(authors, hasItem(obj3));
// need exactly list item but in any order
assertThat(authors, containsInAnyOrder(obj3, obj1, obj2));
}
/**
* [
* "Java",
* "React",
* "JavaScript"
* ]
*/
@Test
public void testList() throws Exception {
mvc.perform(get("/list")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
// $ refer to root element
.andExpect(jsonPath("$", hasSize(3)))
// $[0] refer to first element of the list
.andExpect(jsonPath("$[0]").value("Java"))
.andExpect(jsonPath("$[1]").value("React"))
.andExpect(jsonPath("$[2]").value("JavaScript"))
// normally list order is not fix, better use hasItem
// if contains a specific value
.andExpect(jsonPath("$", hasItem("React")));
}
/**
* {
* "key1": "a",
* "key2": "b",
* "key3": "c"
* }
*/
@Test
public void testMap() throws Exception {
mvc.perform(get("/map")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.key1").value("a"))
.andExpect(jsonPath("$.key2").value("b"))
.andExpect(jsonPath("$.key3").value("c"));
// Deserialize and assert to test the map size, is there a better way?
MvcResult result = mvc.perform(get("/map")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
// convert JSON to Map object
String content = result.getResponse().getContentAsString();
Map<String, Object> resultMap = new ObjectMapper().readValue(content, new TypeReference<>() {
});
assertEquals(3, resultMap.size());
}
}
6. Download Source Code
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-boot-test-json
$ mvn test
$ mvn spring-boot:run