Back to Repositories

Testing HTTP URL Fetcher Response Handling in Glide Library

This test suite validates the HTTP URL fetching functionality in Glide’s image loading library, focusing on server response handling and URL redirection scenarios. It verifies proper handling of HTTP status codes, redirects, timeouts, and header management.

Test Coverage Overview

The test suite provides comprehensive coverage of HTTP URL fetching operations:

  • HTTP response status code handling (200, 301, 302, 400, 500)
  • URL redirect scenarios including relative and chained redirects
  • Timeout behavior and connection management
  • Custom header application and validation
  • Error handling for invalid responses and redirect loops

Implementation Analysis

The testing approach utilizes MockWebServer to simulate HTTP responses and validate request-response patterns. The implementation leverages JUnit and Robolectric frameworks, with Mockito for mocking dependencies. The tests systematically verify both success and failure scenarios using controlled HTTP interactions.

Technical Details

Key technical components include:

  • MockWebServer for HTTP request/response simulation
  • Robolectric test runner for Android environment simulation
  • JUnit 4 testing framework
  • Mockito for mocking and verification
  • Custom timeout and connection factory configuration

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Proper test isolation using @Before and @After methods
  • Comprehensive error case coverage
  • Clear separation of concerns between connection behavior and response handling
  • Effective use of mock objects and argument captors
  • Systematic validation of edge cases and error conditions

bumptech/glide

library/test/src/test/java/com/bumptech/glide/load/data/HttpUrlFetcherServerTest.java

            
package com.bumptech.glide.load.data;

import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.bumptech.glide.Priority;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.Headers;
import com.bumptech.glide.testutil.TestUtil;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import okhttp3.mockwebserver.RecordedRequest;
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.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

/**
 * Tests {@link com.bumptech.glide.load.data.HttpUrlFetcher} against server responses. Tests for
 * behavior (connection/disconnection/options) should go in {@link
 * com.bumptech.glide.load.data.HttpUrlFetcherTest}, response handling should go here.
 */
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE, sdk = ROBOLECTRIC_SDK)
public class HttpUrlFetcherServerTest {
  private static final String DEFAULT_PATH = "/fakepath";
  private static final int TIMEOUT_TIME_MS = 300;

  @Mock private DataFetcher.DataCallback<InputStream> callback;

  private MockWebServer mockWebServer;
  private boolean defaultFollowRedirects;
  private ArgumentCaptor<InputStream> streamCaptor;

  @Before
  public void setUp() throws IOException {
    MockitoAnnotations.initMocks(this);
    defaultFollowRedirects = HttpURLConnection.getFollowRedirects();
    HttpURLConnection.setFollowRedirects(false);
    mockWebServer = new MockWebServer();
    mockWebServer.start();

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

  @After
  public void tearDown() throws IOException {
    HttpURLConnection.setFollowRedirects(defaultFollowRedirects);
    mockWebServer.shutdown();
  }

  @Test
  public void testReturnsInputStreamOnStatusOk() throws Exception {
    String expected = "fakedata";
    mockWebServer.enqueue(new MockResponse().setBody(expected).setResponseCode(200));
    HttpUrlFetcher fetcher = getFetcher();
    fetcher.loadData(Priority.HIGH, callback);
    verify(callback).onDataReady(streamCaptor.capture());
    TestUtil.assertStreamOf(expected, streamCaptor.getValue());
    assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET");
  }

  @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);
    verify(callback).onDataReady(streamCaptor.capture());
    TestUtil.assertStreamOf(expected, streamCaptor.getValue());
    assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET");
    assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET");
  }

  @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);
    verify(callback).onDataReady(streamCaptor.capture());
    TestUtil.assertStreamOf(expected, streamCaptor.getValue());
    assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET");
    assertThat(mockWebServer.takeRequest().getMethod()).isEqualTo("GET");
  }

  @Test
  public void testHandlesRelativeRedirects() throws Exception {
    String expected = "fakedata";
    mockWebServer.enqueue(
        new MockResponse().setResponseCode(301).setHeader("Location", "/redirect"));
    mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(expected));
    getFetcher().loadData(Priority.NORMAL, callback);
    verify(callback).onDataReady(streamCaptor.capture());
    TestUtil.assertStreamOf(expected, streamCaptor.getValue());

    RecordedRequest first = mockWebServer.takeRequest();
    assertThat(first.getMethod()).isEqualTo("GET");
    RecordedRequest second = mockWebServer.takeRequest();
    assertThat(second.getPath()).endsWith("/redirect");
    assertThat(second.getMethod()).isEqualTo("GET");
  }

  @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);
    verify(callback).onDataReady(streamCaptor.capture());
    TestUtil.assertStreamOf(expected, streamCaptor.getValue());

    RecordedRequest request = mockWebServer.takeRequest();
    assertThat(request.getPath()).contains(DEFAULT_PATH);
    assertThat(request.getMethod()).isEqualTo("GET");
    for (int i = 0; i < numRedirects; i++) {
      RecordedRequest current = mockWebServer.takeRequest();
      assertThat(current.getPath()).contains(redirectBase + i);
      assertThat(current.getMethod()).isEqualTo("GET");
    }
  }

  @Test
  public void testFailsOnRedirectLoops() throws Exception {
    mockWebServer.enqueue(
        new MockResponse()
            .setResponseCode(301)
            .setHeader("Location", mockWebServer.url("/redirect").toString()));
    mockWebServer.enqueue(
        new MockResponse()
            .setResponseCode(301)
            .setHeader("Location", mockWebServer.url("/redirect").toString()));

    getFetcher().loadData(Priority.IMMEDIATE, callback);

    verify(callback).onLoadFailed(isA(IOException.class));
  }

  @Test
  public void testFailsIfRedirectLocationIsNotPresent() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(301));

    getFetcher().loadData(Priority.NORMAL, callback);

    verify(callback).onLoadFailed(isA(IOException.class));
  }

  @Test
  public void testFailsIfRedirectLocationIsPresentAndEmpty() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(301).setHeader("Location", ""));

    getFetcher().loadData(Priority.NORMAL, callback);

    verify(callback).onLoadFailed(isA(IOException.class));
  }

  @Test
  public void testFailsIfStatusCodeIsNegativeOne() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(-1));
    getFetcher().loadData(Priority.LOW, callback);

    verify(callback).onLoadFailed(isA(IOException.class));
  }

  @Test
  public void testFailsAfterTooManyRedirects() throws Exception {
    for (int i = 0; i < 10; i++) {
      mockWebServer.enqueue(
          new MockResponse()
              .setResponseCode(301)
              .setHeader("Location", mockWebServer.url("/redirect" + i).toString()));
    }
    getFetcher().loadData(Priority.NORMAL, callback);

    verify(callback).onLoadFailed(isA(IOException.class));
  }

  @Test
  public void testFailsIfStatusCodeIs500() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(500));
    getFetcher().loadData(Priority.NORMAL, callback);

    verify(callback).onLoadFailed(isA(IOException.class));
  }

  @Test
  public void testFailsIfStatusCodeIs400() throws Exception {
    mockWebServer.enqueue(new MockResponse().setResponseCode(400));
    getFetcher().loadData(Priority.LOW, callback);

    verify(callback).onLoadFailed(isA(IOException.class));
  }

  @Test
  public void testSetsReadTimeout() throws Exception {
    MockWebServer tempWebServer = new MockWebServer();
    tempWebServer.enqueue(
        new MockResponse().setBody("test").throttleBody(1, TIMEOUT_TIME_MS, TimeUnit.MILLISECONDS));
    tempWebServer.start();

    try {
      getFetcher().loadData(Priority.HIGH, callback);
    } finally {
      tempWebServer.shutdown();
      // shutdown() called before any enqueue() blocks until it times out.
      mockWebServer.enqueue(new MockResponse().setResponseCode(200));
    }

    verify(callback).onLoadFailed(isA(IOException.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);

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

  private HttpUrlFetcher getFetcher() {
    return getFetcher(Headers.DEFAULT);
  }

  private HttpUrlFetcher getFetcher(Headers headers) {
    URL url = mockWebServer.url(DEFAULT_PATH).url();
    return new HttpUrlFetcher(
        new GlideUrl(url, headers), TIMEOUT_TIME_MS, HttpUrlFetcher.DEFAULT_CONNECTION_FACTORY);
  }
}