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

4

5

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?

Trouveur answered 26/10, 2015 at 17:5 Comment(0)
F
5

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")
            )
        )
    )
);
Flagstaff answered 27/10, 2015 at 8:1 Comment(5)
Thanks for the callout! +1, and you might also be able to do this inline by parameterizing asList (i.e. Arrays.<? super String>>asList(...)).Naidanaiditch
@JeffBowman great. I will add it to the answer, hope you don't mindFlagstaff
Please do! I didn't get a chance to test it, but you articulated a very good answer with a very specific explanation, and should definitely get due credit for that.Naidanaiditch
Overloading is not the cause; see my comment to Jeff's answer.Jawbreaker
Note that in newer versions of Mockito MockitHamcrest#argThat rather than Mockito#argThat should be used.Croner
D
5

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"))
            )
        )
    );
Description answered 20/11, 2017 at 3:23 Comment(0)
N
2

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")))));
    
Naidanaiditch answered 26/10, 2015 at 18:19 Comment(3)
Wow... I wouldn't have thought of that. I didn't realize that the compiler would be missing this due to the overloading issue. I'll run some quick tests and figure out which is the correct statement. Thx.Trouveur
It turns out that overloading is not the cause of the issue. We can see that by adding two dummy methods to the test class, and using them instead of the Hamcrest matchers: static <T> Matcher<Iterable<T>> hasItems(T... items) { return null; } and static <T> Matcher<Iterable<? extends T>> contains(T... items) { return null; }, where using the first with argThat compiles fine, the second does not. The real cause is the use of ? extends T in the second matcher.Jawbreaker
Yes, that's a good summary: Incompatible bounded generics are the root cause. Please read the OP's error message and you'll see why overloads are a confusing part of the symptoms.Naidanaiditch
J
2

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.
    }};
}
Jollenta answered 27/10, 2015 at 20:32 Comment(4)
The Mockito analogue is ArgumentCaptor, for which generic captures may be more-easily-expressed with @Captor. Beware that you may have to work harder to ignore non-matching method calls the way verify does automatically.Naidanaiditch
@JeffBowman JMockit's withCapture() is equivalent to Mockito's ArgumentCaptor, I believe; they both support generic types. I am not sure what you meant by "work harder to ignore non-matching method calls", though; isn't that the same with both APIs, as well?Jawbreaker
The question has a Mockito tag, not JMockit, so I was offering the equivalent for the asker's sake. I imagine both capturing solutions are the same, but compared to a solution where you pass a Matcher into Mockito, any capturing solution gets complicated when there are multiple calls to the same method. Then your captor will have multiple values to check, not just one, and you'll need to assert that any of them match.Naidanaiditch
@JeffBowman The question's title says "Mockito/JMockit" (and it now has the "jmockit" tag). Now, of course using argument capture does not allow the test to restrict which calls it will match, but that "limitation" is the same with both Mockito and JMockit. So, there is no difference between "my captor" and the one a Mockito test would have; that's all I was saying.Jawbreaker

© 2022 - 2024 — McMap. All rights reserved.