Back to Repositories

Testing RequestFutureTarget Async Operations in Glide

This test suite thoroughly validates the RequestFutureTarget implementation in Glide’s request handling system, focusing on asynchronous image loading functionality and future-based operations. The tests ensure proper behavior for resource loading, cancellation, timeouts, and error handling scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of RequestFutureTarget functionality including:
  • Resource loading and completion states
  • Cancellation handling and state management
  • Timeout behavior and thread interruption
  • Error handling and exception propagation
  • Size callback verification

Implementation Analysis

The testing approach employs JUnit and Robolectric frameworks with extensive use of Mockito for mocking dependencies. Tests validate both synchronous and asynchronous behaviors, utilizing mock callbacks and waiter patterns to verify proper request handling and resource delivery.

The implementation demonstrates thorough verification of future-based operations with precise timing controls and state management.

Technical Details

Testing tools and configuration:
  • JUnit 4 test framework
  • Robolectric for Android runtime simulation
  • Mockito for mocking and verification
  • Custom Waiter implementation for async operations
  • SDK configuration for Robolectric environment

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Comprehensive setup and teardown management
  • Thorough edge case coverage
  • Clear test method naming and organization
  • Proper exception testing
  • Effective use of mocking and verification
  • Isolation of async behavior testing

bumptech/glide

library/test/src/test/java/com/bumptech/glide/request/RequestFutureTargetTest.java

            
package com.bumptech.glide.request;

import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.request.target.SizeReadyCallback;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = ROBOLECTRIC_SDK)
public class RequestFutureTargetTest {
  private int width;
  private int height;
  private RequestFutureTarget<Object> future;
  private Request request;
  private RequestFutureTarget.Waiter waiter;

  @Before
  public void setUp() {
    width = 100;
    height = 100;
    waiter = mock(RequestFutureTarget.Waiter.class);
    future = new RequestFutureTarget<>(width, height, false, waiter);
    request = mock(Request.class);
    future.setRequest(request);
  }

  @Test
  public void testCallsSizeReadyCallbackOnGetSize() {
    SizeReadyCallback cb = mock(SizeReadyCallback.class);
    future.getSize(cb);
    verify(cb).onSizeReady(eq(width), eq(height));
  }

  @Test
  public void testReturnsFalseForDoneBeforeDone() {
    assertFalse(future.isDone());
  }

  @Test
  public void testReturnsTrueFromIsDoneIfDone() {
    future.onResourceReady(
        /* resource= */ new Object(),
        /* model= */ null,
        /* target= */ future,
        DataSource.DATA_DISK_CACHE,
        true /*isFirstResource*/);
    assertTrue(future.isDone());
  }

  @Test
  public void testReturnsFalseForIsCancelledBeforeCancelled() {
    assertFalse(future.isCancelled());
  }

  @Test
  public void testReturnsTrueFromCancelIfNotYetDone() {
    assertTrue(future.cancel(false));
  }

  @Test
  public void cancel_withMayInterruptIfRunningTrueAndNotFinishedRequest_clearsFuture() {
    future.cancel(true);

    verify(request).clear();
  }

  @Test
  public void cancel_withInterruptFalseAndNotFinishedRequest_doesNotClearFuture() {
    future.cancel(false);

    verify(request, never()).clear();
  }

  @Test
  public void testDoesNotRepeatedlyClearRequestIfCancelledRepeatedly() {
    future.cancel(true);
    future.cancel(true);

    verify(request, times(1)).clear();
  }

  @Test
  public void testDoesNotClearRequestIfCancelledAfterDone() {
    future.onResourceReady(
        /* resource= */ new Object(),
        /* model= */ null,
        /* target= */ future,
        DataSource.DATA_DISK_CACHE,
        true /*isFirstResource*/);
    future.cancel(true);

    verify(request, never()).clear();
  }

  @Test
  public void testReturnsTrueFromDoneIfCancelled() {
    future.cancel(true);
    assertTrue(future.isDone());
  }

  @Test
  public void testReturnsFalseFromIsCancelledIfCancelledAfterDone() {
    future.onResourceReady(
        /* resource= */ new Object(),
        /* model= */ null,
        /* target= */ future,
        DataSource.DATA_DISK_CACHE,
        true /*isFirstResource*/);
    future.cancel(true);

    assertFalse(future.isCancelled());
  }

  @Test
  public void testReturnsTrueFromCancelIfCancelled() {
    future.cancel(true);
    assertTrue(future.isCancelled());
  }

  @Test
  public void testReturnsFalseFromCancelIfDone() {
    future.onResourceReady(
        /* resource= */ new Object(),
        /* model= */ null,
        /* target= */ future,
        DataSource.DATA_DISK_CACHE,
        true /*isFirstResource*/);
    assertFalse(future.cancel(true));
  }

  @Test
  public void testReturnsResourceOnGetIfAlreadyDone()
      throws ExecutionException, InterruptedException {
    Object expected = new Object();
    future.onResourceReady(
        /* resource= */ expected,
        /* model= */ null,
        /* target= */ future,
        DataSource.DATA_DISK_CACHE,
        true /*isFirstResource*/);

    assertEquals(expected, future.get());
  }

  @Test
  public void testReturnsResourceOnGetWithTimeoutIfAlreadyDone()
      throws InterruptedException, ExecutionException, TimeoutException {
    Object expected = new Object();
    future.onResourceReady(
        /* resource= */ expected,
        /* model= */ null,
        /* target= */ future,
        DataSource.DATA_DISK_CACHE,
        true /*isFirstResource*/);

    assertEquals(expected, future.get(1, TimeUnit.MILLISECONDS));
  }

  @Test(expected = CancellationException.class)
  public void testThrowsCancellationExceptionIfCancelledBeforeGet()
      throws ExecutionException, InterruptedException {
    future.cancel(true);
    future.get();
  }

  @Test(expected = CancellationException.class)
  public void testThrowsCancellationExceptionIfCancelledBeforeGetWithTimeout()
      throws InterruptedException, ExecutionException, TimeoutException {
    future.cancel(true);
    future.get(100, TimeUnit.MILLISECONDS);
  }

  @Test(expected = ExecutionException.class)
  public void testThrowsExecutionExceptionOnGetIfExceptionBeforeGet()
      throws ExecutionException, InterruptedException {
    future.onLoadFailed(/* e= */ null, /* model= */ null, future, /* isFirstResource= */ true);
    future.get();
  }

  @Test(expected = ExecutionException.class)
  public void testThrowsExecutionExceptionOnGetIfExceptionWithNullValueBeforeGet()
      throws ExecutionException, InterruptedException, TimeoutException {
    future.onLoadFailed(/* e= */ null, /* model= */ null, future, /* isFirstResource= */ true);
    future.get(100, TimeUnit.MILLISECONDS);
  }

  @Test(expected = ExecutionException.class)
  public void testThrowsExecutionExceptionOnGetIfExceptionBeforeGetWithTimeout()
      throws ExecutionException, InterruptedException, TimeoutException {
    future.onLoadFailed(/* e= */ null, /* model= */ null, future, /* isFirstResource= */ true);
    future.get(100, TimeUnit.MILLISECONDS);
  }

  @Test(expected = TimeoutException.class)
  public void testThrowsTimeoutExceptionOnGetIfFailedToReceiveResourceInTime()
      throws InterruptedException, ExecutionException, TimeoutException {
    future.get(1, TimeUnit.MILLISECONDS);
  }

  @Test(expected = IllegalArgumentException.class)
  public void testThrowsExceptionIfGetCalledOnMainThread()
      throws ExecutionException, InterruptedException {
    future = new RequestFutureTarget<>(width, height, true, waiter);
    future.get();
  }

  @Test
  public void testGetSucceedsOnMainThreadIfDone() throws ExecutionException, InterruptedException {
    future = new RequestFutureTarget<>(width, height, true, waiter);
    future.onResourceReady(
        /* resource= */ new Object(),
        /* model= */ null,
        /* target= */ future,
        DataSource.DATA_DISK_CACHE,
        true /*isFirstResource*/);
    future.get();
  }

  @Test(expected = InterruptedException.class)
  public void testThrowsInterruptedExceptionIfThreadInterruptedWhenDoneWaiting()
      throws InterruptedException, ExecutionException {
    doAnswer(
            new Answer<Void>() {
              @Override
              public Void answer(InvocationOnMock invocationOnMock) {
                Thread.currentThread().interrupt();
                return null;
              }
            })
        .when(waiter)
        .waitForTimeout(eq(future), anyLong());

    future.get();
  }

  @Test(expected = ExecutionException.class)
  public void testThrowsExecutionExceptionIfLoadFailsWhileWaiting()
      throws ExecutionException, InterruptedException {
    doAnswer(
            new Answer<Void>() {
              @Override
              public Void answer(InvocationOnMock invocationOnMock) {
                future.onLoadFailed(
                    /* e= */ null, /* model= */ null, future, /* isFirstResource= */ true);
                return null;
              }
            })
        .when(waiter)
        .waitForTimeout(eq(future), anyLong());
    future.get();
  }

  @Test(expected = CancellationException.class)
  public void testThrowsCancellationExceptionIfCancelledWhileWaiting()
      throws ExecutionException, InterruptedException {
    doAnswer(
            new Answer<Void>() {
              @Override
              public Void answer(InvocationOnMock invocationOnMock) {
                future.cancel(false);
                return null;
              }
            })
        .when(waiter)
        .waitForTimeout(eq(future), anyLong());
    future.get();
  }

  @Test(expected = TimeoutException.class)
  public void testThrowsTimeoutExceptionIfFinishesWaitingWithTimeoutAndDoesNotReceiveResult()
      throws ExecutionException, InterruptedException, TimeoutException {
    future.get(1, TimeUnit.MILLISECONDS);
  }

  @Test(expected = AssertionError.class)
  public void testThrowsAssertionErrorIfFinishesWaitingWithoutTimeoutAndDoesNotReceiveResult()
      throws ExecutionException, InterruptedException {
    future.get();
  }

  @Test
  public void testNotifiesAllWhenLoadFails() {
    future.onLoadFailed(/* e= */ null, /* model= */ null, future, /* isFirstResource= */ true);
    verify(waiter).notifyAll(eq(future));
  }

  @Test
  public void testNotifiesAllWhenResourceReady() {
    future.onResourceReady(
        /* resource= */ new Object(),
        /* model= */ null,
        /* target= */ future,
        DataSource.DATA_DISK_CACHE,
        true /*isFirstResource*/);
    verify(waiter).notifyAll(eq(future));
  }

  @Test
  public void testNotifiesAllOnCancelIfNotCancelled() {
    future.cancel(false);
    verify(waiter).notifyAll(eq(future));
  }

  @Test
  public void testDoesNotNotifyAllOnSecondCancel() {
    future.cancel(true);
    verify(waiter).notifyAll(eq(future));
    future.cancel(true);
    verify(waiter, times(1)).notifyAll(eq(future));
  }

  @Test
  public void testReturnsResourceIfReceivedWhileWaiting()
      throws ExecutionException, InterruptedException {
    final Object expected = new Object();
    doAnswer(
            new Answer<Void>() {
              @Override
              public Void answer(InvocationOnMock invocationOnMock) {
                future.onResourceReady(
                    /* resource= */ expected,
                    /* model= */ null,
                    /* target= */ future,
                    DataSource.DATA_DISK_CACHE,
                    true /*isFirstResource*/);
                return null;
              }
            })
        .when(waiter)
        .waitForTimeout(eq(future), anyLong());
    assertEquals(expected, future.get());
  }

  @Test
  public void testWaitsForeverIfNoTimeoutSet() throws InterruptedException {
    try {
      future.get();
    } catch (ExecutionException e) {
      throw new RuntimeException(e);
    } catch (AssertionError e) {
      // Expected.
    }
    verify(waiter).waitForTimeout(eq(future), eq(0L));
  }

  @Test
  public void testWaitsForGivenTimeoutMillisIfTimeoutSet() throws InterruptedException {
    long timeout = 2;
    try {
      future.get(timeout, TimeUnit.MILLISECONDS);
    } catch (InterruptedException | ExecutionException e) {
      throw new RuntimeException(e);
    } catch (TimeoutException e) {
      // Expected.
    }

    verify(waiter, atLeastOnce()).waitForTimeout(eq(future), eq(timeout));
  }

  @Test
  public void testConvertsOtherTimeUnitsToMillisForWaiter() throws InterruptedException {
    long timeoutMicros = 1000;
    try {
      future.get(timeoutMicros, TimeUnit.MICROSECONDS);
    } catch (InterruptedException | ExecutionException e) {
      throw new RuntimeException(e);
    } catch (TimeoutException e) {
      // Expected.
    }

    verify(waiter, atLeastOnce())
        .waitForTimeout(eq(future), eq(TimeUnit.MICROSECONDS.toMillis(timeoutMicros)));
  }

  @Test
  public void testDoesNotWaitIfGivenTimeOutEqualToZero() throws InterruptedException {
    try {
      future.get(0, TimeUnit.MILLISECONDS);
    } catch (InterruptedException | ExecutionException e) {
      throw new RuntimeException(e);
    } catch (TimeoutException e) {
      // Expected.
    }

    verify(waiter, never()).waitForTimeout(eq(future), anyLong());
  }
}