Back to Repositories

Testing Volley Stream Fetcher Integration in Bumptech Glide

This test suite validates the VolleyStreamFetcher component in Glide’s Volley integration, focusing on server response handling and HTTP request behaviors. It ensures proper handling of different HTTP status codes, redirects, and header management when fetching image streams.

Test Coverage Overview

The test suite provides comprehensive coverage of VolleyStreamFetcher’s server interaction capabilities.

Key areas tested include:
  • Successful HTTP 200 response handling
  • HTTP redirect chains (301 and 302)
  • Error response handling (400, 500)
  • Custom header application
  • Maximum redirect limit enforcement

Implementation Analysis

The testing approach utilizes MockWebServer to simulate various server responses and validate Volley’s HTTP client behavior. The implementation employs JUnit’s test runner with Robolectric for Android environment simulation, featuring sophisticated request queue management and response verification patterns.

Notable patterns include CountDownLatch for async operations and ArgumentCaptor for stream validation.

Technical Details

Testing tools and configuration:
  • MockWebServer for HTTP request/response simulation
  • Robolectric TestRunner with SDK 19
  • Mockito for mocking and verification
  • Custom SystemClock shadow implementation
  • Volley RequestQueue for network operations

Best Practices Demonstrated

The test suite exemplifies high-quality integration testing practices.

Notable practices include:
  • Proper test setup and teardown management
  • Comprehensive error case coverage
  • Async operation handling
  • Clean mock object management
  • Effective use of test utilities and helpers

bumptech/glide

integration/volley/src/test/java/com/bumptech/glide/integration/volley/VolleyStreamFetcherServerTest.java

            
package com.bumptech.glide.integration.volley;

import static com.bumptech.glide.testutil.TestUtil.assertStreamOf;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.os.SystemClock;
import androidx.test.core.app.ApplicationProvider;
import com.android.volley.RequestQueue;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.Volley;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.Headers;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowSystemClock;

/**
 * Tests {@link com.bumptech.glide.integration.volley.VolleyStreamFetcher} against server responses.
 */
@RunWith(RobolectricTestRunner.class)
@Config(
    manifest = Config.NONE,
    sdk = 19,
    shadows = VolleyStreamFetcherServerTest.FakeSystemClock.class)
public class VolleyStreamFetcherServerTest {
  private static final String DEFAULT_PATH = "/fakepath";

  @Mock private DataFetcher.DataCallback<InputStream> callback;

  private MockWebServer mockWebServer;
  private RequestQueue requestQueue;
  private ArgumentCaptor<InputStream> streamCaptor;
  private CountDownLatch waitForResponseLatch;

  @Before
  public void setUp() throws IOException {
    MockitoAnnotations.initMocks(this);

    waitForResponseLatch = new CountDownLatch(1);
    doAnswer(new CountDown()).when(callback).onDataReady(any(InputStream.class));
    doAnswer(new CountDown()).when(callback).onLoadFailed(any(Exception.class));
    requestQueue = Volley.newRequestQueue(ApplicationProvider.getApplicationContext());
    mockWebServer = new MockWebServer();
    mockWebServer.start();

    streamCaptor = ArgumentCaptor.forClass(InputStream.class);
  }

  @After
  public void tearDown() throws IOException {
    mockWebServer.shutdown();
    requestQueue.stop();
  }

  @Test
  public void testReturnsInputStreamOnStatusOk() throws Exception {
    String expected = "fakedata";
    mockWebServer.enqueue(new MockResponse().setBody(expected).setResponseCode(200));
    DataFetcher<InputStream> fetcher = getFetcher();
    fetcher.loadData(Priority.HIGH, callback);
    waitForResponseLatch.await();
    verify(callback).onDataReady(streamCaptor.capture());
    assertStreamOf(expected, streamCaptor.getValue());
  }

  @Test
  public void testHandlesRedirect301s() throws Exception {
    String expected = "fakedata";
    mockWebServer.enqueue(
        new MockResponse()
            .setResponseCode(301)
            .setHeader("Location", mockWebServer.url("/redirect").toString()));
    mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected));
    getFetcher().loadData(Priority.LOW, callback);
    waitForResponseLatch.await();
    verify(callback).onDataReady(streamCaptor.capture());
    assertStreamOf(expected, streamCaptor.getValue());
  }

  @Test
  public void testHandlesRedirect302s() throws Exception {
    String expected = "fakedata";
    mockWebServer.enqueue(
        new MockResponse()
            .setResponseCode(302)
            .setHeader("Location", mockWebServer.url("/redirect").toString()));
    mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected));
    getFetcher().loadData(Priority.LOW, callback);
    waitForResponseLatch.await();
    verify(callback).onDataReady(streamCaptor.capture());
    assertStreamOf(expected, streamCaptor.getValue());
  }

  @Test
  public void testHandlesUpToFiveRedirects() throws Exception {
    int numRedirects = 4;
    String expected = "redirectedData";
    String redirectBase = "/redirect";
    for (int i = 0; i < numRedirects; i++) {
      mockWebServer.enqueue(
          new MockResponse()
              .setResponseCode(301)
              .setHeader("Location", mockWebServer.url(redirectBase + i).toString()));
    }
    mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected));

    getFetcher().loadData(Priority.NORMAL, callback);
    waitForResponseLatch.await();
    verify(callback).onDataReady(streamCaptor.capture());
    assertStreamOf(expected, streamCaptor.getValue());

    assertThat(mockWebServer.takeRequest().getPath()).contains(DEFAULT_PATH);
    for (int i = 0; i < numRedirects; i++) {
      assertThat(mockWebServer.takeRequest().getPath()).contains(redirectBase + i);
    }
  }

  @Test
  public void testCallsLoadFailedIfRedirectLocationIsEmpty() throws Exception {
    for (int i = 0; i < 2; i++) {
      mockWebServer.enqueue(new MockResponse().setResponseCode(301));
    }

    getFetcher().loadData(Priority.NORMAL, callback);
    waitForResponseLatch.await();
    verify(callback).onLoadFailed(isA(VolleyError.class));
  }

  @Test
  public void testCallsLoadFailedIfStatusCodeIsNegativeOne() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(-1));
    getFetcher().loadData(Priority.LOW, callback);
    waitForResponseLatch.await();
    verify(callback).onLoadFailed(isA(VolleyError.class));
  }

  @Test
  public void testCallsLoadFailedAfterTooManyRedirects() throws Exception {
    for (int i = 0; i < 20; i++) {
      mockWebServer.enqueue(
          new MockResponse()
              .setResponseCode(301)
              .setHeader("Location", mockWebServer.url("/redirect" + i).toString()));
    }
    getFetcher().loadData(Priority.NORMAL, callback);
    waitForResponseLatch.await();
    verify(callback).onLoadFailed(isA(VolleyError.class));
  }

  @Test
  public void testCallsLoadFailedIfStatusCodeIs500() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(500).setBody("error"));
    getFetcher().loadData(Priority.NORMAL, callback);
    waitForResponseLatch.await();
    verify(callback).onLoadFailed(isA(VolleyError.class));
  }

  @Test
  public void testCallsLoadFailedIfStatusCodeIs400() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(400).setBody("error"));
    getFetcher().loadData(Priority.LOW, callback);
    waitForResponseLatch.await();
    verify(callback).onLoadFailed(isA(VolleyError.class));
  }

  @Test
  public void testAppliesHeadersInGlideUrl() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(200));
    String headerField = "field";
    String headerValue = "value";
    Map<String, String> headersMap = new HashMap<>();
    headersMap.put(headerField, headerValue);
    Headers headers = mock(Headers.class);
    when(headers.getHeaders()).thenReturn(headersMap);

    getFetcher(headers).loadData(Priority.HIGH, callback);
    waitForResponseLatch.await();

    assertThat(mockWebServer.takeRequest().getHeader(headerField)).isEqualTo(headerValue);
  }

  private DataFetcher<InputStream> getFetcher() {
    return getFetcher(Headers.DEFAULT);
  }

  private DataFetcher<InputStream> getFetcher(Headers headers) {
    URL url = mockWebServer.url(DEFAULT_PATH).url();
    return new VolleyStreamFetcher(requestQueue, new GlideUrl(url.toString(), headers));
  }

  private class CountDown implements Answer<Void> {

    @Override
    public Void answer(InvocationOnMock invocation) throws Throwable {
      waitForResponseLatch.countDown();
      return null;
    }
  }

  /** A shadow clock that doesn't rely on running on an Android thread with a Looper. */
  @Implements(SystemClock.class)
  public static class FakeSystemClock extends ShadowSystemClock {

    // Used by Shadow.
    @SuppressWarnings("unused")
    @Implementation
    public static long elapsedRealtime() {
      // The default is to return something using the main looper, which doesn't exist on
      // Volley's threads.
      return System.currentTimeMillis();
    }
  }
}