简体   繁体   中英

mockito verify interactions with ArgumentCaptor

To check the number of interactions with a mock where the parameter in the method call is of a certain type, one can do

mock.someMethod(new FirstClass());
mock.someMethod(new OtherClass());
verify(mock, times(1)).someMethod(isA(FirstClass.class));

This will pass thanks to the call to isA since someMethod was called twice but only once with argument FirstClass

However, this pattern seems to not be possible when using an ArgumentCaptor, even if the Captor was created for the particular argument FirstClass

this doesn't work

mock.someMethod(new FirstClass());
mock.someMethod(new OtherClass());
ArgumentCaptor<FirstClass> captor = ArgumentCaptor.forClass(FirstClass.class);
verify(mock, times(1)).someMethod(captor.capture());

it says the mock was called more than once.

Is there any way to accomplish this verification while capturing the argument for further checking?

I recommend using Mockito's Hamcrest integration to write a good, clean matcher for it. That allows you to combine the verification with detailed checking of the passed argument:

verify(mock, times(1)).someMethod(argThat(personNamed("Bob")));

Matcher<Person> personNamed(final String name) {
    return new TypeSafeMatcher<Person>() {
        public boolean matchesSafely(Person item) {
            return name.equals(item.getName());
        }
        public void describeTo(Description description) {
            description.appendText("a Person named " + name);
        }
    };
}

Matchers generally lead to more readable tests and more useful test failure messages. They also tend to be very reusable, and you'll find yourself building up a library of them tailored for testing your project. Finally, you can also use them for normal test assertions using JUnit's Assert.assertThat() , so you get double use out of them.

Quoting the docs:

Note that an ArgumentCaptor don't do any type checks , it is only there to avoid casting in your code. This might however change (type checks could be added) in a future major release.

I wouldn't use an ArgumentCaptor for this. This class captures (literally) everything, despite what class was provided as it's .forClass argument.

To achieve what you want I suggest intercept the argument using Mockito's Answer interface:

private FirstClass lastArgument;

@Test
public void captureFirstClass() throws Exception {
    doAnswer(captureLastArgument()).when(mock).someMethod(anInstanceOfFirstClass());
    mock.someMethod(new FirstClass());
    mock.someMethod(new OtherClass());

    verify(mock, times(1)).someMethod(anInstanceOfFirstClass());
    //write your desired matchers against lastArgument object
}

private Answer<FirstClass> captureLastArgument() {
    return new Answer<FirstClass>() {
        @Override
        public FirstClass answer(InvocationOnMock invocation) throws Throwable {
            TestClass.this.lastArgument = (FirstClass) invocation.getArguments()[0];
            return null;
        }
    };
}

private static Object anInstanceOfFirstClass(){
    return Mockito.argThat(isA(FirstClass.class));
}

You can use the the captor for the sake of capturing, then verify the number of invocations with each argument type separately.

    // given
    ArgumentCaptor<AA> captor = ArgumentCaptor.forClass(AA.class);
    CC cc = new CC();
    // when
    cut.someMethod(new AA());
    cut.someMethod(new BB());
    cut.someMethod(new BB());
    cut.someMethod(cc);
    // then
    Mockito.verify(collaborator, atLeastOnce()).someMethod(captor.capture());
    Mockito.verify(collaborator, times(1)).someMethod(isA(AA.class));
    Mockito.verify(collaborator, times(2)).someMethod(isA(BB.class));
    Mockito.verify(collaborator, times(1)).someMethod(isA(CC.class));
    assertEquals(cc, captor.getValue());

Apparently the generic type of the captor reference doesn't affect anything at runtime.

I also encountered this problem today. I thought I could simply do something like

verify(mock).someMethod(and(isA(FirstClass.class), captor.capture()));

but I couldn't get it to work. I ended up with this solution:

@Test
public void Test() throws Exception {
    final ArgumentCaptor<FirstClass> captor = ArgumentCaptor.forClass(FirstClass.class);

    mock.someMethod(new FirstClass());
    mock.someMethod(new OtherClass());

    verify(eventBus, atLeastOnce()).post(captor.capture());
    final List<FirstClass> capturedValues = typeCheckedValues(captor.getAllValues(), FirstClass.class);
    assertThat(capturedValues.size(), is(1));
    final FirstClass capturedValue = capturedValues.get(0);
    // Do assertions on capturedValue
}

private static <T> List<T> typeCheckedValues(List<T> values, Class<T> clazz) {
    final List<T> typeCheckedValues = new ArrayList<>();
    for (final T value : values) {
        if (clazz.isInstance(value)) {
            typeCheckedValues.add(value);
        }
    }
    return typeCheckedValues;
}

Note: if only one class needs to be captured in this way typeCheckedValues can be simplified into:

private static List<FirstClass> typeCheckedValues(List<FirstClass> values) {
    final List<FirstClass> typeCheckedValues = new ArrayList<>();
    for (final Object value : values) {
        if (value instanceof FirstClass) {
            typeCheckedValues.add((FirstClass) value);
        }
    }
    return typeCheckedValues;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM