76

I have some code

service.doAction(request, Callback<Response> callback);

How can I using Mockito grab the callback object, and call callback.reply(x)

Kurru
  • 14,180
  • 18
  • 64
  • 84

4 Answers4

89

You want to set up an Answer object that does that. Have a look at the Mockito documentation, at https://static.javadoc.io/org.mockito/mockito-core/2.8.47/org/mockito/Mockito.html#answer_stubs

You might write something like

when(mockService.doAction(any(Request.class), any(Callback.class))).thenAnswer(
    new Answer<Object>() {
        Object answer(InvocationOnMock invocation) {
            ((Callback<Response>) invocation.getArguments()[1]).reply(x);
            return null;
        }
});

(replacing x with whatever it ought to be, of course)

Dawood ibn Kareem
  • 77,785
  • 15
  • 98
  • 110
  • 42
    Btw, in case the function returns void, it is needed to do a doAnswer(...).when(mockedObject).method(any(Callback[].class)); as explained at http://stackoverflow.com/questions/3581754/using-mockito-how-do-i-intercept-a-callback-object-on-a-void-method – Thomas Jul 25 '13 at 08:52
  • Here's another link to something similar https://testing.googleblog.com/2014/03/whenhow-to-use-mockito-answer.html – Matt Jul 10 '17 at 00:29
59

Consider using an ArgumentCaptor, which in any case is a closer match to "grab[bing] the callback object".

/**
 * Captor for Response callbacks. Populated by MockitoAnnotations.initMocks().
 * You can also use ArgumentCaptor.forClass(Callback.class) but you'd have to
 * cast it due to the type parameter.
 */
@Captor ArgumentCaptor<Callback<Response>> callbackCaptor;

@Test public void testDoAction() {
  // Cause service.doAction to be called

  // Now call callback. ArgumentCaptor.capture() works like a matcher.
  verify(service).doAction(eq(request), callbackCaptor.capture());

  assertTrue(/* some assertion about the state before the callback is called */);

  // Once you're satisfied, trigger the reply on callbackCaptor.getValue().
  callbackCaptor.getValue().reply(x);

  assertTrue(/* some assertion about the state after the callback is called */);
}

While an Answer is a good idea when the callback needs to return immediately (read: synchronously), it also introduces the overhead of creating an anonymous inner class, and unsafely casting the elements from invocation.getArguments()[n] to the data type you want. It also requires you to make any assertions about the pre-callback state of the system from WITHIN the Answer, which means that your Answer may grow in size and scope.

Instead, treat your callback asynchronously: Capture the Callback object passed to your service using an ArgumentCaptor. Now you can make all of your assertions at the test method level and call reply when you choose. This is of particular use if your service is responsible for multiple simultaneous callbacks, because you have more control over the order in which the callbacks return.

oligofren
  • 20,744
  • 16
  • 93
  • 180
Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • but can you do that if the method with the callback is being called indirectly by the test (in other words, it is called by something which is triggered by the test code but not in the test code)? – Thomas Jul 25 '13 at 08:43
  • Generally, if you're working with a callback in a unit test, either you're passing in an interface for your system under test to call (that you can mock) or you're mocking a dependency's method that _receives_ a callback (so you have to call the callback yourself, as above). It's atypical to ever hand the captured callback interface off to yet another class to call, but there's no reason you couldn't. Is that what you meant? – Jeff Bowman Jul 25 '13 at 15:06
  • You can avoid the type safety issue by using `invocation.getArgumentAt(1, Callback.class).reply();` – Steve Jun 23 '17 at 15:19
  • @Steve That doesn't avoid the issue, that just hides the cast in a method call. You are still asserting at runtime that `arguments[1]` is a Callback, not an Object, which is more than the compiler can guarantee itself. Not a big deal, but it is a difference. – Jeff Bowman Jun 23 '17 at 15:34
11

If you have a method like:

public void registerListener(final IListener listener) {
    container.registerListener(new IListener() {
        @Override
        public void beforeCompletion() {
        }

        @Override
        public void afterCompletion(boolean succeeded) {
            listener.afterCompletion(succeeded);
        }
    });
}

Then following way you can mock the above method easily:

@Mock private IListener listener;

@Test
public void test_registerListener() {
    target.registerListener(listener);

    ArgumentCaptor<IListener> listenerCaptor =
            ArgumentCaptor.forClass(IListener.class);

    verify(container).registerListener(listenerCaptor.capture());

    listenerCaptor.getValue().afterCompletion(true);

    verify(listener).afterCompletion(true);
}

I hope this might help someone, as I had spend lot of time in figuring out this solution.

GoRoS
  • 5,183
  • 2
  • 43
  • 66
Sumit Kumar Saha
  • 799
  • 1
  • 12
  • 25
  • I'm getting nullPointerException at network check. can you please share any article or tutorial for api testing in android? – akshay bhange Mar 05 '19 at 12:22
2
when(service.doAction(any(Request.class), any(Callback.class))).thenAnswer(
    new Answer() {
    Object answer(InvocationOnMock invocation) {
        Callback<Response> callback =
                     (Callback<Response>) invocation.getArguments()[1];
        callback.reply(/*response*/);
    }
});
Garrett Hall
  • 29,524
  • 10
  • 61
  • 76