Android code will typically make use of async callback functions to do work that could take a long time, such as making network calls, or interacting with the file store or local databases. In fact the Android OS will throw an exception if you attempt to make a network call from the UI thread, which means that a common pattern of writing code is call a synchronous method and pass a callback function that will get called asynchronously when the long running operation is complete.

For example this is the kind of code that gets written

private void unpackFromUrlAsync(Intent intent) {
 view.showProgress(R.string.progress_unpacking_intent);
 subscription = unpackerFromUrl.
    getRecommendationFromUrl(
      intent, 
      new ITaskResultHandler<Recommendation>() { 
        @Override
        public void onSuccess(Recommendation result) {
          view.hideProgress();
          view.showRecommendation(result);
        }

        @Override
        public void onError(ITaskErrorResult error) {
          view.hideProgress();
          view.showErrorMessage(error);
          view.end();
        }
      });
}

@Override
public void unpackIntent(Intent intent) {
 Recommendation recommendation = null;
 switch (intentTypeFinder.getTypeOfContent(intent)) {
  case Url:
    unpackFromUrlAsync(intent);
    return;
  case Text:
    recommendation = unpackerFromText.getRecommendationFromText(intent);
    if (recommendation != null) {
      view.showRecommendation(recommendation);
    }
    else {
      view.showMessage(R.string.error_bad_share_receive);
      view.end();
    }
    return;
  }
  view.showMessage(R.string.unknown_intent_type);
  view.end();
}

So as you can see the flow of this code is that when we are unpacking a Url intent we ask the view to ShowProgress then we make the getRecommendationFromUrl passing in the ITaskResultHandler async callback functions.

We can easily test the synchronous side of the code like this.

@Before
public void setup() {
 setupPresenter(true);
}

@Test
public void unpack_if_url() {
  // arrange
  Intent intent = new Intent("UNKNOWN");
  IIntentContentTypeFinder typeFinder = getApplicationComponent().provideIntentContentTypeFinder();
  when(typeFinder.getTypeOfContent(intent)).thenReturn(IntentContentType.Url);
  IIntentUnpackerFromUrl unpacker = getApplicationComponent().provideIntentUnpackerUrl();
 
  // act
  presenter.unpackIntent(intent);
 
  // assert
  // this test only checks we start the async processing not that it completes
  verify(mockView, never()).end();
  verify(mockView, times(1)).showProgress(R.string.progress_unpacking_intent);
  verify(unpacker, times(1)).
    getRecommendationFromUrl(
      any(Intent.class), 
      Matchers.<ITaskResultHandler<Recommendation>>any());
}

Testing that the async operation, getRecommendationFromUrl, completes is more complex.

One approach would be to create a concrete object that implemented the IIntentUnpackerFromUrl interface and that concrete object could them store the callback method when getRecommendationFromUrl is called. Then we could use that concrete object to trigger the callback. However doing this makes the setup of the components more complex, sometimes we want a mock object in the unit test component and sometimes we would want the concrete test object. Also we end up creating test objects where untested code can live and the test objects have to be kept in step with the real implementation.

So, an alternative approach is to use mockito Captors. They enable the test to capture parameters passed to the mock, in this case the callback function and then we can examine, or call that captured parameter.

public class UnpackIntentTests extends PresenterTestSetup {

private Recommendation recommendation;
private IIntentUnpackerFromUrl mockUnpackerFromUrl;
private Subscription mockSubscription;

@Captor
private ArgumentCaptor<ITaskResultHandler<Recommendation>> intentFromUrlCallbackCaptor;

@Before
public void setup() {
  // so we can use generic captors - this links to the @Captor above
  MockitoAnnotations.initMocks(this);

  setupPresenter(true);

  recommendation = new Recommendation();
  mockSubscription = mock(Subscription.class);
  mockUnpackerFromUrl = applicationComponent.provideIntentUnpackerUrl();
  // we want to capture the async callback setup call
  when(mockUnpackerFromUrl
    .getRecommendationFromUrl(
      any(Intent.class),
      intentFromUrlCallbackCaptor.capture()))
        .thenReturn(mockSubscription);
}

// this will get the captured callback function parameter
private ITaskResultHandler<recommendation> getIntentFromUrlCallback() {
  List<ITaskResultHandler<Recommendation>> capturedCallbacks =
    intentFromUrlCallbackCaptor.getAllValues();

  assertNotNull(
    "callback handler has not been set by the code under test but we are attempting to call it",
    capturedCallbacks);
  assertThat(
    "callback handler has not been set by the code under test but we are attempting to call it",
    capturedCallbacks.size(),
    is(greaterThan(0)));

  return capturedCallbacks.get(0);
}

@Test
public void unpack_if_url_succeeds() {
  // arrange
  Intent intent = new Intent("UNKNOWN");
  IIntentContentTypeFinder typeFinder = getApplicationComponent().provideIntentContentTypeFinder();
  when(typeFinder.getTypeOfContent(intent)).thenReturn(IntentContentType.Url);
  presenter.unpackIntent(intent);
 
  // act
  getIntentFromUrlCallback().onSuccess(recommendation);
 
  // assert
  verify(mockView, times(1)).showRecommendation(recommendation);
  verify(mockView, times(1)).hideProgress();
}
 
@Test
public void exit_if_url_fails() {
  // arrange
  Intent intent = new Intent("UNKNOWN");
  IIntentContentTypeFinder typeFinder = getApplicationComponent().provideIntentContentTypeFinder();
  when(typeFinder.getTypeOfContent(intent)).thenReturn(IntentContentType.Url);
  presenter.unpackIntent(intent);
 
  // act
  getIntentFromUrlCallback().onError(new TaskErrorResult(987));
 
  // assert
  verify(mockView, times(1)).showErrorMessage(any(TaskErrorResult.class));
  verify(mockView, times(1)).hideProgress();
  verify(mockView, times(1)).end();
}

The two tests trigger the callback succeeding and failing.