Main Tutorials

Testing JSON in Spring Boot

spring boot test json

This article shows how to use MockMvc and JsonPath to test JSON in Spring Boot.

Table of Contents:

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.

Terminal

  <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>
Terminal

$ 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.

Terminal

  curl /endpoint

  {"name" : "hello world"}

In unit tests, we can use JSONPath expressions to navigate through JSON data and extract values for testing.

MyControllerTest.java

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.

Terminal

 [
    "Java",
    "React",
    "JavaScript"
 ]

And we can use the same jsonPath to test the JSON List.

testList.java

  @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.

Terminal

{
  "key1": "a",
  "key2": "b",
  "key3": "c"
}

And we can use the same jsonPath to test the Map.

.java

  @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).

WebController.java

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.

SimpleBook.java

package com.mkyong.model;

public class SimpleBook {
  private String title;

  public SimpleBook(String title) {
      this.title = title;
  }

  //getters and setters
}
Author.java

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
}
Book.java

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.

WebControllerTest.java

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

7. References

About Author

author image
Founder of Mkyong.com, love Java and open source stuff. Follow him on Twitter. If you like my tutorials, consider make a donation to these charities.

Comments

Subscribe
Notify of
0 Comments
Inline Feedbacks
View all comments