English 中文(简体)
Using Mockito, how do I match against the key-value pair of a map?
原标题:
  • 时间:2010-04-05 18:52:56
  •  标签:
  • java
  • mockito

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?

问题回答

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);
        }
    }
}

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"); 
....

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.

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.

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





相关问题
Spring Properties File

Hi have this j2ee web application developed using spring framework. I have a problem with rendering mnessages in nihongo characters from the properties file. I tried converting the file to ascii using ...

Logging a global ID in multiple components

I have a system which contains multiple applications connected together using JMS and Spring Integration. Messages get sent along a chain of applications. [App A] -> [App B] -> [App C] We set a ...

Java Library Size

If I m given two Java Libraries in Jar format, 1 having no bells and whistles, and the other having lots of them that will mostly go unused.... my question is: How will the larger, mostly unused ...

How to get the Array Class for a given Class in Java?

I have a Class variable that holds a certain type and I need to get a variable that holds the corresponding array class. The best I could come up with is this: Class arrayOfFooClass = java.lang....

SQLite , Derby vs file system

I m working on a Java desktop application that reads and writes from/to different files. I think a better solution would be to replace the file system by a SQLite database. How hard is it to migrate ...

热门标签