56

I'm running into a generics problem with Mockito and Hamcrest.

Please assume the following interface:

public interface Service {
    void perform(Collection<String> elements);
}

And the following test snippet:

Service service = mock(Service.class);

// ... perform business logic

verify(service).perform(Matchers.argThat(contains("a", "b")));

So I want to verify that my business logic actually called the service with a collection that contains "a" and "b" in that order.

However, the return type of contains(...) is Matcher<Iterable<? extends E>>, so Matchers.argThat(...) returns Iterable<String> in my case, which naturally does not apply to the required Collection<String>.

I know that I could use an argument captor as proposed in Hamcrest hasItem and Mockito verify inconsistency, but I would very much like not to.

Any suggestions! Thanks!

Robert Moskal
  • 21,737
  • 8
  • 62
  • 86
Philipp Jardas
  • 3,222
  • 3
  • 29
  • 42
  • Casting a `Matcher>` to `Matcher>`? That surely won't compile... – Philipp Jardas Dec 07 '13 at 13:31
  • If you know collection type and collection has **correct** `equals` implementation then you can call `verify` with collection instance that contains 'a' and 'b'. But this would be bad test - first you will explode implementation details as well you have to relay on correct equals method. So I would use argument captor without doubts – Eugen Martynov Dec 07 '13 at 14:57
  • I don't know how far you would go to avoid argument captors, but maybe you could implement a custom matcher like `IsListOfTwoElements` here: http://docs.mockito.googlecode.com/hg/org/mockito/ArgumentMatcher.html – Katona Dec 07 '13 at 19:09
  • Thank you for your suggestions. Unsurprisingly, I don't like either of them. Guess I'll have to use those argument captors after all. :-( – Philipp Jardas Dec 08 '13 at 20:32
  • Can't you just write `verify(service).perform((Collection) Matchers.argThat(contains("a", "b")));` ? – Dawood ibn Kareem Dec 09 '13 at 08:55
  • 1
    You cannot verify "in order" on `Collection` since `Collection` is not guaranteed to be an ordered collection. Are you sure you meant Collection and not `List`? – Kevin Welker Dec 09 '13 at 21:04
  • @KevinWelker, assume `containsInAnyOrder` then, which has the same signature. – Philipp Jardas Dec 11 '13 at 13:49

7 Answers7

40

You can just write

verify(service).perform((Collection<String>) Matchers.argThat(contains("a", "b")));

From the compiler's point of view, this is casting an Iterable<String> to a Collection<String> which is fine, because the latter is a subtype of the former. At run time, argThat will return null, so that can be passed to perform without a ClassCastException. The important point about it is that the matcher gets onto Mockito's internal structure of arguments for verification, which is what argThat does.

Dawood ibn Kareem
  • 77,785
  • 15
  • 98
  • 110
  • 1
    `contains` returns a `Matcher>`, not a `Matcher>`, so without `@SuppressWarnings` you're going to get an unsafe cast warning in there somewhere. Same goes for `containsInAnyOrder`, which would also work. – Jeff Bowman Dec 11 '13 at 23:39
  • 8
    which contains do you use? i only finde contains(String substring) – barracuda317 Jun 06 '18 at 12:08
  • Just so as to keep this up to date; an easy way to do this with Kotlin is: `verify(service).perform(argThat { "b" == this["a"] })` – d4vidi Feb 11 '19 at 12:34
  • @barracuda317 they are using org.hamcrest.Matchers.contains(). I managed to solve this using org.hamcrest.CoreMatchers.hasItems() instead. – Stephen Ruda Oct 08 '19 at 19:05
  • 2
    Since mockito 2.1.0 you need to use MockitoHamcrest.argThat. https://javadoc.io/doc/org.mockito/mockito-core/2.2.9/org/mockito/ArgumentMatcher.html – findusl Jan 08 '20 at 17:00
13

As an alternative one could change the approach to ArgumentCaptor:

@SuppressWarnings("unchecked") // needed because of `List<String>.class` is not a thing
// suppression can be worked around by using @Captor on a field
ArgumentCaptor<List<String>> captor = ArgumentCaptor.forClass(List.class);

verify(service).perform(captor.capture());
assertThat(captor.getValue(), contains("a", "b"));

Notice, that as a side effect this decouples the verification from the Hamcrest library, and allows you to use any other library (e.g. Truth):

assertThat(captor.getValue()).containsExactly("a", "b");
TWiStErRob
  • 44,762
  • 26
  • 170
  • 254
11

If you get stuck in situations like these, remember that you can write a very small reusable adapter.

verify(service).perform(argThat(isACollectionThat(contains("foo", "bar"))));

private static <T> Matcher<Collection<T>> isACollectionThat(
    final Matcher<Iterable<? extends T>> matcher) {
  return new BaseMatcher<Collection<T>>() {
    @Override public boolean matches(Object item) {
      return matcher.matches(item);
    }

    @Override public void describeTo(Description description) {
      matcher.describeTo(description);
    }
  };
}

Note that David's solution above, with casting, is the shortest right answer.

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • @David You should. :) You're welcome to, after the fact, for the proper credit. – Jeff Bowman Dec 10 '13 at 16:24
  • thanks for your answer and the hint that Mockito matchers return `null`. If @DavidWallace does add his comment as a reply, I will accept his answer though. Hope you won't mind. :-) – Philipp Jardas Dec 11 '13 at 13:51
  • @Philupp It's only fair! He just did answer, FWIW, so I'm editing down mine to avoid the repetition. – Jeff Bowman Dec 11 '13 at 16:41
11

You can put your own lambda as an ArgumentMatcher

when(myClass.myMethod(argThat(arg -> arg.containsAll(asList(1,2))))
    .thenReturn(...);
dehasi
  • 2,644
  • 1
  • 19
  • 31
1

Why not just verify with the expected arguments, assuming the list only contains the two items, e.g.:

final List<String> expected = Lists.newArrayList("a", "b");
verify(service).perform(expected);

Whilst I agree with Eugen in principle, I think that relying on equals for String comparison is acceptable... besides, the contains matcher uses equals for comparison anyway.

Jonathan
  • 20,053
  • 6
  • 63
  • 70
0

Similar to another answer here you can do the following:

verify(yourmock, times(1)).yourmethod(argThat(arg -> arg.containsAll(asList("a", "b"))));
kolobok
  • 3,835
  • 3
  • 38
  • 54
-1

You could have your own java.util.Collection implementation and override the equals method like below.

public interface Service {
    void perform(Collection<String> elements);
}

@Test
public void testName() throws Exception {
    Service service = mock(Service.class);
    service.perform(new HashSet<String>(Arrays.asList("a","b")));
    Mockito.verify(service).perform(Matchers.eq(new CollectionVerifier<String>(Arrays.asList("a","b"))));
}

public class CollectionVerifier<E> extends ArrayList<E> {

    public CollectionVerifier() {

    }

    public CollectionVerifier(final Collection<? extends E> c) {
        super(c);
    }

    @Override
    public boolean equals(final Object o) {
        if (o instanceof Collection<?>) {
            Collection<?> other = (Collection<?>) o;
                return this.size() == other.size() && this.containsAll(other);
        }
        return false;
    }

}
Dev Blanked
  • 8,555
  • 3
  • 26
  • 32