Mockito/JMockit & Hamcrest matchers : How to verify Lists/Collections?

12,022

Solution 1

As Jeff Bowman has already pointed out, the problem is that the compiler doesn't know which of the 4 contains methods you are trying to call.

The list you are constructing

Arrays.asList(Matchers.equalTo("a"), Matchers.equalTo("b"))

is of type

List<Matcher<String>>

but the contains method you want to call (<E> Matcher<Iterable<? extends E>> contains(List<Matcher<? super E>> itemMatchers)) expects a type

List<Matcher<? super String>>

as parameter. As your list type doesn't match the expected one, the compiler actually thinks that you are trying to call

<E> Matcher<Iterable<? extends E>> contains(E... items)

The solution: give the compiler what it wants. Create a List<Matcher<? super String>> instead of a List<Matcher<String>>:

        List<Matcher<? super String>> matchersList = new ArrayList<>();
        matchersList.add(Matchers.equalTo("a"));
        matchersList.add(Matchers.equalTo("b"));

        // no illegal cast anymore
        Mockito.verify(mock).doSomething(
            (Collection<String>) argThat(Matchers.contains(matchersList)));

EDIT:

Adding Jeff Bowman's inline solution from his comment, that enables the use of Arrays.asList as stated in the question:

Mockito.verify(mock).doSomething(
   (Collection<String>) argThat(
        Matchers.contains(
            Arrays.<Matcher<? super String>> asList(
                Matchers.equalTo("a"), Matchers.equalTo("b")
            )
        )
    )
);

Solution 2

I would prefer use allOf

import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;

...

    Mockito.verify(mock).doSomething(
        argThat(
            allOf(
                hasItems(equalTo("a")),
                hasItems(equalTo("b"))
            )
        )
    );

Solution 3

I believe this is due to an annoying ambiguity in Hamcrest, which has on its Matchers class:

  1. <E> Matcher<Iterable<? extends E>> contains(E... items)
  2. <E> Matcher<Iterable<? extends E>> contains(Matcher<? super E> itemMatcher)
  3. <E> Matcher<Iterable<? extends E>> contains(Matcher<? super E>... itemMatchers)
  4. <E> Matcher<Iterable<? extends E>> contains(List<Matcher<? super E>> itemMatchers)

That's right, depending on whether you pass Hamcrest an item, a matcher, a varargs array of matchers, or a list of matchers, you'll get different behavior. Because Java has no aversion to matching lists of Hamcrest matchers, there is plenty of opportunity for one statement to match more than one of those overloads, and the choice between them is the most specific overload as determined by dizzying type algebra in JLS 18.5.4.

I think you're intending item #4 above—pass contains a List of Matcher<E> (Matcher<String>) and get back a Matcher<Iterable<? extends String>>—but the compiler sees it as #1—pass contains a value of type E (List<Matcher<String>>) and get back a Matcher<Iterable<? extends List<Matcher<String>>>>.

There are a couple workarounds, which I haven't tested yet:

  • Extract the Matcher to a variable, which you can do with Hamcrest matchers like contains but not Mockito matchers like argThat:

    Matcher<Iterable<String>> matchesAAndB = Matchers.contains(
        Arrays.asList(Matchers.equalTo("a"), Matchers.equalTo("b")));
    Mockito.verify(mock).doSomething((Collection<String>)argThat(matchesAAndB));
    
  • Choose E explicitly:

    Mockito.verify(mock).doSomething((Collection<String>)argThat(
        Matchers.<String>contains(Arrays.asList(
            Matchers.equalTo("a"), Matchers.equalTo("b")))));
    

Solution 4

The best way is to use the standard assertThat method (from Hamcrest or JUnit), which will work best with any Hamcrest matcher. With JMockit you could then do:

@Test
public void usingJMockit(@Mocked final Collaborator mock) {
    mock.doSomething(asList("a", "b"));

    new Verifications() {{
        List<String> values;
        mock.doSomething(values = withCapture());

        // Now check the list of captured values using JUnit/Hamcrest:
        assertThat(values, contains("a", "b"));

        // Alternatively, could have used Asser4J, FEST Assert, etc.
    }};
}
Share:
12,022

Related videos on Youtube

Eric B.
Author by

Eric B.

Updated on June 04, 2022

Comments

  • Eric B.
    Eric B. almost 2 years

    This 2013 post on SO asked how to use Hamcrest matchers to verify lists/collections invocations in Mockito. The accepted solution was to cast the Matcher to a (Collection).

    I'm trying to do something similar, but running into a class cast error. I am not sure if I am misusing Hamcrest matchers, or if this usage simply isn't supported by Mockito. In my case, I'm trying to use a list of Matchers as my argument:

    static class Collaborator
    {
       void doSomething(Iterable<String> values) {}
    }
    
    @Test
    public void usingMockito()
    {
       Collaborator mock = Mockito.mock(Collaborator.class);
       mock.doSomething(Arrays.asList("a", "b"));
    
       // legal cast
       Mockito.verify(mock).doSomething((Collection<String>)argThat(Matchers.contains("a", "b")));
       // legal cast
       Mockito.verify(mock).doSomething((Collection<String>)argThat(Matchers.contains(Matchers.equalTo("a"), Matchers.equalTo("b"))));
    
       // illegal cast!!! Cannot cast from Iterable<capture#3-of ? extends List<Matcher<String>>> to Collection<String>
       Mockito.verify(mock).doSomething((Collection<String>)argThat(Matchers.contains(Arrays.asList(Matchers.equalTo("a"), Matchers.equalTo("b")))));
    }
    

    But I get the cast error:

    Cannot cast from Iterable<capture#3-of ? extends List<Matcher<String>>> to Collection<String>
    

    Am I doing something unsupported?