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

4

26

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?

Krugersdorp answered 20/3, 2019 at 19:45 Comment(3)
One option is to use "isOneOf("231","1232")" and you can put it to both, but it doesn't guarantee it's not the same in both cases. Another option is to sort your array before you create your resource, so it wouldn't change randomly. Other than that I haven't found a better solution either.Menhaden
Or another option is to remove index and have it like $.id, hasItems("1232", "321"), but you can't tell if the items matchMenhaden
I was thinking about sorting, but I'm still hoping there may exist a simpler one-step way, like it is for an array of strings.Krugersdorp
C
51

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())

Chain answered 20/3, 2019 at 20:4 Comment(4)
That's right. However, in this case I'm not able to tell which of 2 elements belongs to which object, which complicates the matter even more...Krugersdorp
@VasiliyGalkin I've updated the answer with another approach.Chain
thanks for your suggestion. It should work. The problem with it, in real-life code (with 10-20 fields instead of 3) it turns into unreadable mess. So I was hoping there exists a more elegant solution that could be written in a more MockMvc'ish way, with a chain of andExpect or in some similar way...Krugersdorp
Make sure to use Matchers.containsInAnyOrder from org.hamcrest library since there are a lot of classes in various libraries having name MatcherAutoharp
C
9

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.

Canea answered 10/2, 2020 at 1:40 Comment(1)
The problem with strict = false is that it ignores any discrepancies between actual and expected result, not just the order of items in the collection. You could have an empty json in <json-here> and the test would still pass.Blouson
C
2

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.

Cornstalk answered 17/8, 2022 at 0:1 Comment(0)
V
0

How about this solution?

class MyObject() {
   private Long id;
   private String name;
}

@Service
public MyObjectService {

    private List<MyObject> myObjects;

    public List<MyObject> list() {
         return myObjects;
    }
}

@RestController
@RequestMapping("/myObjects")
@RequiredArgsConstructor
public MyObjectController() {
    private final MyObjectService myObjectService;
    
    @GetMapping
    ResponseEntity<ListMyObject> list() {
        List<MyObject> myObjects = myObjectService.list();
        return ResponseEntity.ok(myObjects);
    }
}

@SpringWebTest(MyObjectController.class)
public MyObjectClassTest {

     @Autowired
     private MockMvc MockMvc;
     @Mock
     private MyObjectService myObjectService;

     @Test
     void listShouldReturnMyObjectList()() throw Eception {
          MyObject myObject1 = new MyObject(1, "My object one name");
          MyObject myObject2 = new MyObject(1, "My object two name");
    List<T> objects = List.of(myObject1, myObject2); 
    doReturn(objects).when(myObjectService).list();
    ResultActions resultActions = 
        mockMvc.perform(get("/myObjects")                            
           .andExpect(status().isOk())
           .andExpect(content()
               .contentType(MediaType.APPLICATION_JSON));
    andExpectedMyObjectList(resultActions, myObjects);
}  
              
private ResultActions andExpectedMyObject(ResultActions 
        resultActions, String prefix, MyObject myObject) {
    return resultActions.andExpect(prefix + ".id", 
            is(myObject.getId())))
        .andExpect(prefix + ".name", is(myObject.getName());
} 
                    
private ResultActions andExpectedMyObjectList(ResultActions 
        resultActions, List<MyObject> myObjects) {
    int i = 0;  
    for(MyObject ob : myObjects) {
        resultActions = 
            andExpectedMyObject(resultActions, "$.[" + i++ "]", ob);
    }
    return resultActions;        
}
Varnado answered 29/12, 2023 at 12:56 Comment(2)
It would be helpful to have more details or the actual solution to properly assess it. As it stands, it's hard to rate without understanding the solution being referenced.Resolute
I've modified my answer above.Varnado

© 2022 - 2024 — McMap. All rights reserved.