Deeps
Deeps

Reputation: 327

How to match a Map of objects array in Java

How can I match a Map of Object[] in Mockito? I used

  String[] entity= {"entity1"}
  Map<String,Object[]> queryParam = new HashMap<String,Object[]>();
  queryParam.put("entityString",entity);

and

  when(DB.get("SomeString", queryParam)).thenReturn(mockResponse);

But it is not matching. I feel like it is unable to match Object[] and String [] during real call. Please help me.

Upvotes: 0

Views: 3460

Answers (2)

Deeps
Deeps

Reputation: 327

After struggling for the whole day, I figured out a solution. As specified by @Jeff Bowman, Mockito can't match Object to String in real call. I followed this link and wrote a custom matcher that can compare the real arguments to our specified, if matches returns true, then Mockito mocks the call otherwise false. This is that solution

      private class Query4ArgumentMatcher extends ArgumentMatcher<Map<String, Object[]>> {
     private String value;


    public Query4ArgumentMatcher(String value) {

        this.value = value;

    }

    public boolean matches(Object o) {

        if (o instanceof Map) {

            Map<String, Object[]> map = (Map<String, Object[]>) o;


            for (Map.Entry<String, Object[]> m : map.entrySet()) {

                String s = (m.getValue())[0].toString();

                if (value.contentEquals(s)) {

                    return true;

                }

            }

        }

        return false;

    }

}

and in unit test

 when(DB.get(eq("SomeString"), Mockito.argThat(new Query4ArgumentMatcher("entity1"))));

Hope this will help someone.

Upvotes: 0

Jeff Bowman
Jeff Bowman

Reputation: 95654

Clarified answer (matching a Map)

It is now clear that what you're trying to do is match a Map<String, Object[]> instead of mocking one, which can be tricky: Even though Map supports equals, arrays compare by identity instead of deep equality by default. In this case I would use a few Hamcrest matchers:

private Matcher<Map<String, Object[]>> hasParamArray(
    String key, Object... vals) {
  return hasEntry(
      equalTo(key),
      arrayContaining(vals));
}

// elsewhere
when(DB.get(
        eq("someString"),
        argThat(hasParamArray("entityString", "entity1"))))
    .thenReturn(mockResponse);

As an alternative, write an Answer:

when(DB.get(eq("someString"), anyMap())).thenAnswer(new Answer<Response>() {
  @Override public void answer(InvocationOnMock invocation) {
    Map<String, Object[]> map =
        (Map<String, Object[]>) invocation.getArguments()[1];
    if (/* ... */) {
      return mockResponse1;
    } else if (/* ... */) {
      return mockResponse2;
    } else {
      return defaultResponse;
    }
  }
});

Original answer (mocking a Map)

Note that Object[] and String[] are not compatible types:

Map<String, Object[]> yourMap = new HashMap<>();
String[] yourStringArray = new String[] { "hello" };

// this is a compile error, or at least it should be...
yourMap.put("foo", yourStringArray);

// ...because this would otherwise be a runtime error, which is probably
// what you're experiencing right now.
Object[] yourObjectArray = yourMap.get("foo"); // returns yourStringArray
yourObjectArray[0] = new Object();

return yourStringArray[0].length();  // oops! this is that new Object()!

You can probably switch entity to type Object[] and be done:

// in your class with a call to MockitoAnnotations.initMocks(this) in setUp()
@Mock Map<String, Object[]> queryParam;

when(queryParam.get("someString")).thenReturn(new Object[] { "entity1" });

That said, I would advise doing as David said in the comments, and use a real map instead. The rule of thumb is don't mock data objects, and there are three very good reasons not to mock Map<String, Object[]>:

  1. Mocks are never going to be as good as the real thing. Interfaces can change, as they did to support streams in Java 8, and real implementations will adapt to these changes where mocks will not. This is especially true when mocking real objects, where an addition of a final modifier (for instance) will ruin your mock without actually affecting your system under test.

  2. Unlike a database, a Map is very easy to manually create. Many database systems have many dependencies, and require a lot of memory. Maps have no additional dependencies, and are extraordinarily lightweight and well-tested core components of Java.

    (Even if it were a full database, an in-memory "fake" or some other standalone implementation would be much more robust than any reasonable Mockito mock. Because the JDK provides you with many reasonable implementations, it's an even easier choice.)

  3. To appropriately mock a Map takes an equivalent amount of work, or more. Your test will likely get certain values out of the map, each of which would need to be stubbed with when; a mutable Map can simply accept those with put. Should the Map be mutated by your system, you'd need to anticipate the call and update the mock, where a real Map will have the correct behavior automatically.

    This is to say nothing of the calls containsKey(K), containsValue(V), remove(Object), size(), and the many other methods on Map that you'd need to replace to make a robust test. With a mock, the simplest and most reasonable code changes will break your test, unless you mock everything at great expense of time and readability.

In short, a Map is a much better choice here than any Mockito mock.

Upvotes: 1

Related Questions