Using Mockito's ArgumentCaptor class to match a child class
Asked Answered
T

4

11

The below code shows my problem. Effectively, I am trying to use Mockito's ArgumentCaptor to verify that a method was called once with a certain concrete class. I would like to use ArgumentCaptor here if possible, but I am beginning to suspect I need to use a custom ArgumentMatcher instead.

The problem is that the line Mockito.verify(mocked).receive(captor.capture()); (Edit: Added this to the code below) fails with a TooManyActualInvocations exception (2 instead of 1). I would like to understand why this is happening - is it poor implementation of Mockito or a limitation caused by type erasure of generics?

public class FooReceiver {
  public void receive(Foo foo) {

  }
}

public interface Foo {
}

public class A implements Foo {
}

public class B implements Foo {
}

public class TestedClass {
  private FooReceiver receiver;
  public TestedClass(FooReceiver receiver) {
    this.receiver = receiver;
  }

  public void doStuff() {
    receiver.receive(new A());
    receiver.receive(new B());
  }
}

public class MyTest {

  @Test
  public void testingStuff() {
    // Setup
    FooReceiver mocked = Mockito.mock(FooReceiver.class);
    TestedClass t = new TestedClass(mocked);

    // Method under test
    t.doStuff();

    // Verify
    ArgumentCaptor<B> captor = ArgumentCaptor.forClass(B.class);
    Mockito.verify(mocked).receive(captor.capture()); // Fails here

    Assert.assertTrue("What happened?", captor.getValue() instanceof B);
  }
}

EDIT: For anyone interested, I ended up doing this:

// Verify
final B[] b = new B[1];
ArgumentMatcher<B> filter = new ArgumentMatcher<B>() {
  @Override
  public boolean matches(Object argument) {
    if(argument instanceof B) {
      b[0] = (B) argument;
      return true;
    }
    return false;
  }
}
Mockito.verify(mocked).receive(Mockito.argThat(filter));
Tholos answered 23/3, 2011 at 10:19 Comment(0)
D
6

You can also use Mockito.isA to verify that the argument is of a specific class:

verify(mock).init(isA(ExpectedClass.class));

Mockito JavaDoc

Diorama answered 11/7, 2012 at 1:43 Comment(2)
That's what I was looking for! Is that Matcher new?Tholos
Great! Not sure, I've used it for a while.. I remember being as surprised as you when I first heard about it.. :)Diorama
T
7

As far as I can tell this is a limitation / poor implementation. When looking at org.mockito.internal.matchers.CapturingMatcher there is

public boolean matches(Object argument) {
    return true;
}

meaning it matches every argument / class.

This results in org.mockito.internal.matchers.CapturingMatcher#getAllValues returning a List<B> but actually containing one A and one B resulting in a ClassCastException during runtime when trying to get them as B.

List<Object> arguments; // the invocations

// adds a new invocation
public void captureFrom(Object argument) {
    // ... 
    this.arguments.add(argument);
    // ... 
}

// return the list of arguments, using raw types remove any compiler checks for validity,
// the returned List contains elements that are not of type T
public List<T> getAllValues() {
    // ... 
    return new ArrayList<T>((List) arguments);
    // ... 
}

This should be solvable by changing org.mockito.ArgumentCaptor in a way that it passes its Class<? extends T> clazz into the CapturingMatcher and therefore passing the type information along properly, enabling a proper matches implementation and removing the need for the cast / raw type usage.

Tosh answered 8/1, 2018 at 10:7 Comment(0)
D
6

You can also use Mockito.isA to verify that the argument is of a specific class:

verify(mock).init(isA(ExpectedClass.class));

Mockito JavaDoc

Diorama answered 11/7, 2012 at 1:43 Comment(2)
That's what I was looking for! Is that Matcher new?Tholos
Great! Not sure, I've used it for a while.. I remember being as surprised as you when I first heard about it.. :)Diorama
C
0

The method will be called twice so you need to do this:

Mockito.verify(mocked, times(2)).receive(captor.capture());
Cindacindee answered 23/3, 2011 at 12:5 Comment(1)
Thanks for the response. I guess I was hoping that the ArgumentCaptor<B> instance would automatically filter the verify() command in the same way you would with a custom argument matcher that does an instanceof check for the class B.Tholos
U
0
private static class CapturingMatcherB<T> extends CapturingMatcher<T> {
    public boolean matches(Object argument) {
        return argument instanceof B;
    }
}

CapturingMatcherB<B> captor = new CapturingMatcherB<>();
verify(mocked).receive(Mockito.argThat(captor));
Assert.assertTrue("What happened?", captor.getLastValue() instanceof B);

Source: luk2302's answer (implemented it in code)

java didn't seem to like "instanceof T". Using an anonymous class instead of a private-static-class also gave me troubles with the override.

This is a bit shorter and cleaner-looking than the asker's (user545680) clever ArgumentMatcher solution.

Generic version: (bit longer, does not hardcode "B", recommended)

private static class CapturingMatcherGeneric<T> extends CapturingMatcher<T> {
    private final Class<T> typeParameterClass;

    public CapturingMatcherGeneric(Class<T> typeParameterClass) {
        this.typeParameterClass = typeParameterClass;
    }

    public boolean matches(Object argument) {
        return argument.getClass().isAssignableFrom(typeParameterClass);
    }
}

CapturingMatcherGeneric<B> capture = new CapturingMatcherGeneric<>(B.class);
verify(mocked).receive(Mockito.argThat(captor));
Assert.assertTrue("What happened?", captor.getLastValue() instanceof B);

Generic version using ArgumentMatcher/CapturesArguments: (just to show how it works - it is longer)

private static class CapturingMatcherGeneric2<T> implements ArgumentMatcher<T>, CapturesArguments {
    private final Class<T> typeParameterClass;
    private final List<Object> arguments = new ArrayList();

    public CapturingMatcherGeneric2(Class<T> typeParameterClass) {
        this.typeParameterClass = typeParameterClass;
    }

    @Override
    public boolean matches(T argument) {
        return argument.getClass().isAssignableFrom(typeParameterClass);
    }

    @Override
    public void captureFrom(Object argument) {
        this.arguments.add(argument);
    }

    public T getLastValue() {
        return (T) this.arguments.get(this.arguments.size() - 1);
    }
}
Uniformitarian answered 8/2 at 0:39 Comment(1)
This useful approach argThat((ArgumentMatcher<B>) argument -> { ... return true; }) also has the problem that if the argument can be type B or C (eg children of same interface) and both are called, it throws an exception in the matcher. And using ArgumentMatcher<Object> doesn't work for me either because it does not know which overloaded method to use (my method has a couple overloads) - same problem as above "troubles with the override" - error mentions "erasure".Uniformitarian

© 2022 - 2024 — McMap. All rights reserved.