Mockito and Hamcrest: how to verify invocation of Collection argument?
Asked Answered
H

7

59

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!

Halliard answered 7/12, 2013 at 12:53 Comment(7)
Casting a Matcher<Iterable<String>> to Matcher<Collection<String>>? That surely won't compile...Halliard
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 doubtsElata
I don't know how far you would go to avoid argument captors, but maybe you could implement a custom matcher like IsListOfTwoElements here: docs.mockito.googlecode.com/hg/org/mockito/ArgumentMatcher.htmlMoleskin
Thank you for your suggestions. Unsurprisingly, I don't like either of them. Guess I'll have to use those argument captors after all. :-(Halliard
Can't you just write verify(service).perform((Collection<String>) Matchers.argThat(contains("a", "b"))); ?Nolasco
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?Dorison
@KevinWelker, assume containsInAnyOrder then, which has the same signature.Halliard
N
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.

Nolasco answered 11/12, 2013 at 16:33 Comment(5)
contains returns a Matcher<Iterable<? extends String>>, not a Matcher<Iterable<String>>, so without @SuppressWarnings you're going to get an unsafe cast warning in there somewhere. Same goes for containsInAnyOrder, which would also work.Splat
which contains do you use? i only finde contains(String substring)Paquito
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"] })Appraise
@Paquito they are using org.hamcrest.Matchers.contains(). I managed to solve this using org.hamcrest.CoreMatchers.hasItems() instead.Boatswain
Since mockito 2.1.0 you need to use MockitoHamcrest.argThat. javadoc.io/doc/org.mockito/mockito-core/2.2.9/org/mockito/…Gyatt
S
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");
Subteen answered 7/9, 2017 at 16:4 Comment(0)
S
12

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.

Splat answered 9/12, 2013 at 19:5 Comment(2)
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. :-)Halliard
@Philupp It's only fair! He just did answer, FWIW, so I'm editing down mine to avoid the repetition.Splat
A
12

You can put your own lambda as an ArgumentMatcher

when(myClass.myMethod(argThat(arg -> arg.containsAll(asList(1,2))))
    .thenReturn(...);
Ardin answered 30/1, 2019 at 14:10 Comment(1)
Awesome! And if you want to check for an empty collection you could just use argThat(Collection::isEmpty).Gissing
R
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.

Ryannryazan answered 10/12, 2013 at 9:6 Comment(1)
This example depends on the order of elements in the list.Wellbeing
S
1

Similar to another answer here you can do the following:

verify(yourmock, times(1)).yourmethod(argThat(arg -> arg.containsAll(asList("a", "b"))));
Simmie answered 1/7, 2022 at 15:50 Comment(0)
D
-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;
    }

}
Dorcus answered 9/12, 2013 at 12:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.