Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring MockMvc: match a collection of JSON objects in any order

I have an API endpoint which, when called with GET, returns an array of JSON objects in the body, like this:

[
  {"id": "321", "created": "2019-03-01", "updated": "2019-03-15"},
  {"id": "123", "created": "2019-03-02", "updated": "2019-03-16"}
]

I would like to check the body with a Spring MockMvc test case. The statement currently looks like this:

mockMvc.perform(get("/myapi/v1/goodstuff").
  andExpect(status().isOk()).
  andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)).
  andExpect(jsonPath("$.*", isA(ArrayList.class))).
  andExpect(jsonPath("$.*", hasSize(2))).
  andExpect(jsonPath("$[0].id", is("321"))).
  andExpect(jsonPath("$[0].created", is("2019-03-01"))).
  andExpect(jsonPath("$[0].updated*", is("2019-03-15"))).
  andExpect(jsonPath("$[1].id", is("1232"))).
  andExpect(jsonPath("$[1].created", is("2019-03-02"))).
  andExpect(jsonPath("$[1].updated*", is("2019-03-16")));

However, the implementation of my API doesn't guarantee the order of JSON object in the returned array. Were this an array of strings, I would solve this via matcher generated by org.hamcrest.collection.IsIterableContainingInAnyOrder<T>.containsInAnyOrder. But I cannot see any suitable matcher for my situation in their doc, nor any clue in the description of jsonPath method in Spring docs

From a quick search I didn't manage find anything related to my situation on SO, either, beyond a list of strings situation I described above. Of course, I could convert JSON objects to strings.

But I'm wondering, could I solve this problem for a list of JSON objects, comparing each of the fields of each objects one-by-one (like shown in the code snippet above), but ignoring the order of objects in the collection?

Update: Zgurskyi has suggested a solution that helps with my original simplified example. However, with a real-life practical example there are 2 more inputs:

  • the number of fields is 10-20 instead of 3
  • not all of matchers are plain is, for instance:

(a bit closer to my original code)

mockMvc.perform(get("/myapi/v1/greatstuff").
      andExpect(status().isOk()).
      andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)).
      andExpect(jsonPath("$.*", isA(ArrayList.class))).
      andExpect(jsonPath("$.*", hasSize(2))).
      andExpect(jsonPath("$[0].id", is("321"))).
      andExpect(jsonPath("$[0].did", anything())).
      andExpect(jsonPath("$[0].createdTs", startsWith("2019-03-01"))).
      andExpect(jsonPath("$[0].updatedTs", startsWith("2019-03-15"))).
      andExpect(jsonPath("$[0].name", equalToIgnoringCase("wat"))).
      andExpect(jsonPath("$[0].stringValues", containsInAnyOrder("a","b","c"))).
      andExpect(jsonPath("$[1].id", is("1232"))).
      andExpect(jsonPath("$[1].did", anything())).
      andExpect(jsonPath("$[1].createdTs", startsWith("2019-03-01"))).
      andExpect(jsonPath("$[1].updatedTs", startsWith("2019-03-15"))).
      andExpect(jsonPath("$[1].name", equalToIgnoringCase("taw"))).
      andExpect(jsonPath("$[1].stringValues", containsInAnyOrder("d","e","f"))).
      andReturn();

So far it seems that I can't do anything better than implementing my own matcher class.

Or...can I?

like image 315
Vasiliy Galkin Avatar asked Mar 20 '19 19:03

Vasiliy Galkin


3 Answers

You can assert list items fields ignoring order:

.andExpect(jsonPath("$[*].id", containsInAnyOrder("321", "123")))
.andExpect(jsonPath("$[*].created", containsInAnyOrder("2019-03-01", "2019-03-02")))
.andExpect(jsonPath("$[*].updated", containsInAnyOrder("2019-03-15", "2019-03-16")))

Another approach would be to check that specific list items exist in response:

.andExpect(jsonPath("$.[?(@.id == 123 && @.created == \"2019-03-02\" && @.updated == \"2019-03-16\")]").exists())
.andExpect(jsonPath("$.[?(@.id == 321 && @.created == \"2019-03-01\" && @.updated == \"2019-03-15\")]").exists())

like image 159
Oleksii Zghurskyi Avatar answered Oct 23 '22 20:10

Oleksii Zghurskyi


Additionally there is another way to assert the json without being strict about order using MockMvcResultMatchers

.andExpect(MockMvcResultMatchers.content().json(<json-here>, false))

By setting the strict=false, it can do a fussy search.

like image 38
keshav jois Avatar answered Oct 23 '22 18:10

keshav jois


I think a better solution could be something like that:

.andExpect(jsonPath("$.violations", hasSize(3)))
.andExpect(jsonPath("$.violations", containsInAnyOrder(
            Map.of("field", "name", "message", "must not be empty"),
            Map.of("field", "email", "message", "must not be empty"),
            Map.of("field", "birthdate", "message", "must not be null")
          )
))

It worked for me, but I have to be honest, I don't like to use Map instead a domain type, like Violation, Tuple, Category etc. Unfortunately, I could not make it work with a type different than Map.

like image 1
Rafael Ponte Avatar answered Oct 23 '22 20:10

Rafael Ponte