27

I need to send a specific value from a mock object based on a specific key value.

From the concrete class:

map.put("xpath", "PRICE");
search(map);

From the test case:

IOurXMLDocument mock = mock(IOurXMLDocument.class);
when(mock.search(.....need help here).thenReturn("$100.00");

How do I mock this method call for this key value pair?

Marquee
  • 1,776
  • 20
  • 21
Sean
  • 301
  • 1
  • 3
  • 4

5 Answers5

31

I found this trying to solve a similar issue creating a Mockito stub with a Map parameter. I didn't want to write a custom matcher for the Map in question and then I found a more elegant solution: use the additional matchers in hamcrest-library with mockito's argThat:

when(mock.search(argThat(hasEntry("xpath", "PRICE"))).thenReturn("$100.00");

If you need to check against multiple entries then you can use other hamcrest goodies:

when(mock.search(argThat(allOf(hasEntry("xpath", "PRICE"), hasEntry("otherKey", "otherValue")))).thenReturn("$100.00");

This starts to get long with non-trivial maps, so I ended up extracting methods to collect the entry matchers and stuck them in our TestUtils:

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.hasEntry;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.hamcrest.Matcher;
---------------------------------
public static <K, V> Matcher<Map<K, V>> matchesEntriesIn(Map<K, V> map) {
    return allOf(buildMatcherArray(map));
}

public static <K, V> Matcher<Map<K, V>> matchesAnyEntryIn(Map<K, V> map) {
    return anyOf(buildMatcherArray(map));
}

@SuppressWarnings("unchecked")
private static <K, V> Matcher<Map<? extends K, ? extends V>>[] buildMatcherArray(Map<K, V> map) {
    List<Matcher<Map<? extends K, ? extends V>>> entries = new ArrayList<Matcher<Map<? extends K, ? extends V>>>();
    for (K key : map.keySet()) {
        entries.add(hasEntry(key, map.get(key)));
    }
    return entries.toArray(new Matcher[entries.size()]);
}

So I'm left with:

when(mock.search(argThat(matchesEntriesIn(map))).thenReturn("$100.00");
when(mock.search(argThat(matchesAnyEntryIn(map))).thenReturn("$100.00");

There's some ugliness associated with the generics and I'm suppressing one warning, but at least it's DRY and hidden away in the TestUtil.

One last note, beware the embedded hamcrest issues in JUnit 4.10. With Maven, I recommend importing hamcrest-library first and then JUnit 4.11 (now 4.12) and exclude hamcrest-core from JUnit just for good measure:

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-library</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-all</artifactId>
    <version>1.9.5</version>
    <scope>test</scope>
</dependency>

Edit: Sept 1, 2017 - Per some of the comments, I updated my answer to show my Mockito dependency, my imports in the test util, and a junit that is running green as of today:

import static blah.tool.testutil.TestUtil.matchesAnyEntryIn;
import static blah.tool.testutil.TestUtil.matchesEntriesIn;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.util.HashMap;
import java.util.Map;

import org.junit.Test;

public class TestUtilTest {

    @Test
    public void test() {
        Map<Integer, String> expected = new HashMap<Integer, String>();
        expected.put(1, "One");
        expected.put(3, "Three");

        Map<Integer, String> actual = new HashMap<Integer, String>();
        actual.put(1, "One");
        actual.put(2, "Two");

        assertThat(actual, matchesAnyEntryIn(expected));

        expected.remove(3);
        expected.put(2, "Two");
        assertThat(actual, matchesEntriesIn(expected));
    }

    @Test
    public void mockitoTest() {
        SystemUnderTest sut = mock(SystemUnderTest.class);
        Map<Integer, String> expected = new HashMap<Integer, String>();
        expected.put(1, "One");
        expected.put(3, "Three");

        Map<Integer, String> actual = new HashMap<Integer, String>();
        actual.put(1, "One");

        when(sut.search(argThat(matchesAnyEntryIn(expected)))).thenReturn("Response");
        assertThat(sut.search(actual), is("Response"));
    }

    protected class SystemUnderTest {
        // We don't really care what this does
        public String search(Map<Integer, String> map) {
            if (map == null) return null;
            return map.get(0);
        }
    }
}
Marquee
  • 1,776
  • 20
  • 21
  • 3
    That's a handy solution. Curiously though, I was getting a "Wrong argument type, expecting Map found Map extends String, ? extends String>" that java couldn't seem to resolve with any amount of prodding and import tweaking. I had to change my mocked class's method to accept a `Map extends String, ? extends String>` to get it to work. Any thoughts? I have the same dependency versions for junit and hamcrest as you; my Mockito is v. 1.9.5. – Patrick M Apr 08 '15 at 16:37
  • @PatrickM did you manage to resolve the generics issue in a cleaner fashion? From the above I have exactly the same problem but am unable to change the collection generic types... – Andrew Eells Feb 10 '16 at 13:50
  • 1
    @AndrewEells, no, I never found a solution. I switched from Hamcrest and started using AssertJ, which seems to need far less type coercion for its assertions. – Patrick M Feb 10 '16 at 14:39
  • @PatrickM, AndrewEells - I've not seen your issue. I recreated the test util and a test case using the above and it is still working. Curious about your AssertJ solution as well, maybe you could share it as a new answer? – Marquee Sep 01 '17 at 14:02
  • Hmm now that you mention it, AssertJ doesn't really have a solution for removing the requirement on Hamcrest matchers from Mockito's `when` interface. That's where I was having a problem with the type coercion, not the assertions as I indicated in my previous comment. I can't even recall which project I had to do they type tweaking in, much less the unit test. (But I'm pretty sure I had to leave it with the `? extends String` syntax.) – Patrick M Sep 01 '17 at 15:45
  • 1
    An offhand improvement - use anonymous classes init to populate the maps, e.g.: `new HashMap() {{ put(1, "One"); put(2, "Two"); }};` etc. – d4vidi Feb 11 '19 at 11:10
17

If you just want to "match" against a particular Map, you can use some of the answers above, or a custom "matcher" Object that extends Map<X, Y>, or an ArgumentCaptor, like this:

ArgumentCaptor<Map> argumentsCaptured = ArgumentCaptor.forClass(Map.class);
verify(mock, times(1)).method((Map<String, String>) argumentsCaptured.capture());
assert argumentsCaptured.getValue().containsKey("keynameExpected"); 
// argumentsCaptured.getValue() will be the first Map it called it with.
// argumentsCaptured.getAllValues() if it was called more than times(1)

See also more answers here: Verify object attribute value with mockito

If you want to capture multiple maps:

ArgumentCaptor<Map> argumentsCaptured = ArgumentCaptor.forClass(Map.class);
ArgumentCaptor<Map> argumentsCaptured2 = ArgumentCaptor.forClass(Map.class);
verify(mock, times(1)).method(argumentsCaptured.capture(), argumentsCaptured2.capture());
assert argumentsCaptured.getValue().containsKey("keynameExpected"); 
assert argumentsCaptured2.getValue().containsKey("keynameExpected2"); 
....
rogerdpack
  • 62,887
  • 36
  • 269
  • 388
7

For anyone arriving to this question like myself, there's actually a very simple solution based on Lambdas:

when(mock.search(argThat(map -> "PRICE".equals(map.get("xpath"))))).thenReturn("$100.00");

Explanation: argThat expects an ArgumentMatcher which is a functional interface and thus can be written as a Lambda.

Ray
  • 3,084
  • 2
  • 19
  • 27
4

Doesn't this work?

Map<String, String> map = new HashMap<String, String>();
map.put("xpath", "PRICE");
when(mock.search(map)).thenReturn("$100.00");

The Map parameter should behave the same way as other parameters.

Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • There is missing one closing bracket. – stefanglase Apr 05 '10 at 21:49
  • IOurXMLDocument represents our service layer that I do not want to call for my unit tests. For 1 situation we are calling it twice with 2 different map values. Instead I want to inspect the value and return a fixed result. So when the application code does the following: map.put("xpath", "PRODUCTNAME"); when(mock.search(map)).thenReturn("Candybar"); – Sean Apr 06 '10 at 15:54
  • Shouldn't be mock.search(eq(map)) so it check the actual map equality? – fikovnik Nov 27 '15 at 21:50
  • this does not work. Using equality matcher on map does not work too. – drndivoje Sep 10 '18 at 09:19
3

Seems like what you need is an Answer:

IOurXMLDocument doc = mock(IOurXMLDocument.class);
when(doc.search(Matchers.<Map<String,String>>any())).thenAnswer(new Answer<String>() {
    @Override
    public String answer(InvocationOnMock invocation) throws Throwable {
        Map<String, String> map = (Map<String, String>) invocation.getArguments()[0];
        String value = map.get("xpath");
        if ("PRICE".equals(value)) {
            return "$100.00";
        } else if ("PRODUCTNAME".equals(value)) {
            return "Candybar";
        } else {
            return null;
        }
    }
});

But what seems like a better idea is to not use primitive Map as parameter to your search method - you could probably transform this map into a pojo with price and productName attributes. Just an idea :)

denis.solonenko
  • 11,645
  • 2
  • 28
  • 23