Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
174 views
in Technique[技术] by (71.8m points)

How to test json structure in spring boot

I found this code to test if a json string is equal to another string

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcExampleTests {

    @Autowired
    private MockMvc mvc;

    @Test
    public void exampleTest() throws Exception {
        this.mvc.perform(get("/")).andExpect(status().isOk())
                .andExpect(content().string("Hello World"));
    }

}

Do you know how to test the json structure ? Like check if the object contains id, name, age ... whatever the values are. Thanks.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

The best way to verify the correct JSON response depends on what your response looks like.

For example, let's say that you have a @Controller (or @RestController) that accesses a @Repository (either directly or indirectly via a @Service) and maps the resulting item to JSON. For example:

public class MyPojo {

    private int id;
    private String name;
    private int age;

    // no-args for deserialization, required by some Frameworks with default settings
    public MyPojo() {}

    // constructor
    public MyPojo(final int id, final String name, final int age) { ... }
    
    // Getters
    public int getid() { ... }
    public String getName() { ... }
    public int getName() { ... }

}
@Repository
public class MyRepository {

    public MyPojo findMyPojoById(final int id) { return db.findById(id); }

}
@RestController
@RequestMapping("/")
public class MyController {

    @Autowired
    private MyRepository repository;

    @GetMapping
    public MyPojo findMyPojoById(@RequestParam final int id) {
        return repository.findMyPojoByid(id);
    }

}

Then a GET request with ?id=1 would perhaps return something like the following:

{
    "id": 1,
    "name": "John",
    "age": 12
}

In this case, you have multiple choices. In a mockMvc test, you are usually not interested in testing the integration of the components and only wish to test that your controller is working as expected. In this case, you would probably want a unit-test, and mock the repository. You have some options:

1. Via a mocked expected response

In this case you can do something like:

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MockMvcExampleTests {

    @Autowired
    private MockMvc mvc;
    
    @MockBean
    private MyRepository repository; // mock the repository
   
    @Autowired
    private ObjectMapper objectMapper;

    @Test
    public void exampleTest() throws Exception {
        final int userId = 1;
        MyPojo mockedUser = new MyPojo(userId, "John", 12);
        Mockito.doReturn(mockedUser).when(repository).findMyPojoById(userId);
        
        final String expectedResponseContent = objectMapper.writeValueAsString(mockedUser);
 
        this.mvc.perform(get("/"))
            .andExpect(status().isOk())
            .andExpect(content().json(expectedResponseContent ));
    
        verify(repository).findMyPojoById(userId); // verify that the repository was called correctly
    }
}

The upside of this is that the tests are robuster. If your MyPojo object changes, your tests would fail as long as you do not update all constructor usages of your mocked objects in test - a moot point, as the code would not compile until you do.

2. Via jsonPath()

In this case, you can use jsonPath (a way to evaluate expressions on JSON structures, similar to XPath for XML) to individually evaluate some or all parts of a JSON structure.

For example (still mocking the response):

    @Test
    public void exampleTest() throws Exception {
        final int userId = 1;
        MyPojo mockedUser = new MyPojo(userId, "John", 12);
        Mockito.doReturn(mockedUser).when(repository).findMyPojoById(userId); // note that this mock is not necessary, but it does make the test a unit test
         
        this.mvc.perform(get("/"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id", is(userId))) // is(<Object>) is e.g. a Hamcrest Matcher
            .andExpect(jsonPath("$.name", is("John"))
            .andExpect(jsonPath("$.age", is(12)))

        verify(repository).findMyPojoById(userId);
    }

Note that if you had e.g. an array of response objects, you could parse them with the proper array notation, e.g.:

.andExpect(jsonPath("$[0].name", is("Mary")))

The upside of this is that you do not have to evaluate the entire response. This may be useful if, for example, the server response includes server-generated data that is difficult to replicate or mock without doing away with too much of the actual code logic (via e.g. mocking). In this case, you could check for the presence of the field, without regard for its actual value, e.g.:

.andExpect(jsonPath("$.serverGeneratedSecretKey", notNullValue()))

3. Via string matching

Lastly, you might compare the actual response:


String expectedResponse = "{"id": 1, "name":"John", "age": 12}";

String responseString = mockMvc.perform(...) 
                               .andExpect(status().isOk())
                               .andReturn()
                               .getResponse()
                               .getContentAsString();

assertEquals("Response does not match", expectedResponse, responseString);

However, this way is very fickle:

  • You need to take match the JSON exactly, which includes escaping the "'s and proper whitespacing.
  • While e.g. ObjectMapper tries to honor the ordering of the JSON fields as they appear, you should never base your code working based on the ordering of a JSON object (aside from arrays, ordering in JSON is not defined)
  • This approach also makes refactoring a nightmare. Adding a field breaks your test, making you have to change the expected string - error-prone due to the above reasons.

I can't think of a very good use case for this approach, but, well - it's there if you want it.

It might be interesting to note that Java 13 finally supports text blocks. These blocks, while they make it easier to write the expected response string, does not help with the other points.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...