Back to Repositories

Testing HTTP URL Fetching Implementation in Glide

This test suite evaluates the HttpUrlFetcher class in the Glide library, focusing on URL connection handling and data fetching functionality. The tests verify HTTP request behaviors, error handling, and resource cleanup mechanisms for image loading operations.

Test Coverage Overview

The test suite provides comprehensive coverage of HTTP URL fetching operations in Glide.

Key areas tested include:
  • HTTP connection error handling and status codes
  • Stream management and cleanup operations
  • Timeout configurations
  • Cancellation behavior
  • Redirect handling
Edge cases covered include malformed URLs, connection failures, and various HTTP error scenarios.

Implementation Analysis

The testing approach utilizes JUnit with Robolectric for Android environment simulation and Mockito for dependency mocking.

Notable patterns include:
  • Mock HTTP connections and streams
  • ArgumentCaptor usage for exception verification
  • Ordered verification for cleanup sequences
  • Systematic error condition testing

Technical Details

Testing infrastructure includes:
  • JUnit 4 test framework
  • Robolectric test runner with SDK configuration
  • Mockito mocking framework
  • Custom connection factory for URL handling
  • Priority-based data loading simulation

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through:

  • Thorough setup and teardown handling
  • Systematic error case coverage
  • Clear test method naming
  • Proper mock verification
  • Resource cleanup validation
  • Isolation of system under test

bumptech/glide

library/test/src/test/java/com/bumptech/glide/load/data/HttpUrlFetcherTest.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.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.bumptech.glide.Priority;
import com.bumptech.glide.load.HttpException;
import com.bumptech.glide.load.model.GlideUrl;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = ROBOLECTRIC_SDK)
public class HttpUrlFetcherTest {
  @Mock private HttpURLConnection urlConnection;
  @Mock private HttpUrlFetcher.HttpUrlConnectionFactory connectionFactory;
  @Mock private GlideUrl glideUrl;
  @Mock private InputStream stream;
  @Mock private DataFetcher.DataCallback<InputStream> callback;

  private static final int TIMEOUT_MS = 100;
  private HttpUrlFetcher fetcher;

  @Before
  public void setUp() throws IOException {
    MockitoAnnotations.initMocks(this);
    URL url = new URL("http://www.google.com");

    when(connectionFactory.build(eq(url))).thenReturn(urlConnection);
    when(urlConnection.getInputStream()).thenReturn(stream);
    when(urlConnection.getResponseCode()).thenReturn(200);
    when(glideUrl.toURL()).thenReturn(url);

    fetcher = new HttpUrlFetcher(glideUrl, TIMEOUT_MS, connectionFactory);
  }

  @Test
  public void loadData_whenConnectThrowsFileNotFound_notifiesCallbackWithHttpErrorCode()
      throws IOException {
    int statusCode = 400;
    doThrow(new FileNotFoundException()).when(urlConnection).connect();
    when(urlConnection.getResponseCode()).thenReturn(statusCode);

    fetcher.loadData(Priority.HIGH, callback);

    HttpException exception = (HttpException) getCallbackException();
    assertThat(exception.getStatusCode()).isEqualTo(statusCode);
  }

  @Test
  public void loadData_whenGetInputStreamThrows_notifiesCallbackWithStatusCode()
      throws IOException {
    int statusCode = 400;
    doThrow(new IOException()).when(urlConnection).getInputStream();
    when(urlConnection.getResponseCode()).thenReturn(statusCode);

    fetcher.loadData(Priority.HIGH, callback);

    HttpException exception = (HttpException) getCallbackException();
    assertThat(exception.getStatusCode()).isEqualTo(statusCode);
  }

  @Test
  public void loadData_whenConnectAndGetResponseCodeThrow_notifiesCallbackWithInvalidStatusCode()
      throws IOException {
    doThrow(new FileNotFoundException()).when(urlConnection).connect();
    when(urlConnection.getResponseCode()).thenThrow(new IOException());

    fetcher.loadData(Priority.HIGH, callback);

    HttpException exception = (HttpException) getCallbackException();
    assertThat(exception.getStatusCode()).isEqualTo(HttpUrlFetcher.INVALID_STATUS_CODE);
  }

  @Test
  public void loadData_whenRedirectUrlIsMalformed_notifiesCallbackWithStatusCode()
      throws IOException {
    int statusCode = 300;

    when(urlConnection.getHeaderField(eq(HttpUrlFetcher.REDIRECT_HEADER_FIELD)))
        .thenReturn("gg://www.google.com");
    when(urlConnection.getResponseCode()).thenReturn(statusCode);

    fetcher.loadData(Priority.HIGH, callback);

    HttpException exception = (HttpException) getCallbackException();
    assertThat(exception.getStatusCode()).isEqualTo(statusCode);
    assertThat(exception.getCause()).isInstanceOf(MalformedURLException.class);
  }

  private Exception getCallbackException() {
    ArgumentCaptor<Exception> captor = ArgumentCaptor.forClass(Exception.class);
    verify(callback).onLoadFailed(captor.capture());
    return captor.getValue();
  }

  @Test
  public void testSetsReadTimeout() {
    fetcher.loadData(Priority.HIGH, callback);
    verify(urlConnection).setReadTimeout(eq(TIMEOUT_MS));
  }

  @Test
  public void testSetsConnectTimeout() {
    fetcher.loadData(Priority.IMMEDIATE, callback);
    verify(urlConnection).setConnectTimeout(eq(TIMEOUT_MS));
  }

  @Test
  public void testReturnsNullIfCancelledBeforeConnects() throws IOException {
    InputStream notExpected = new ByteArrayInputStream(new byte[0]);
    when(urlConnection.getInputStream()).thenReturn(notExpected);

    fetcher.cancel();
    fetcher.loadData(Priority.LOW, callback);
    verify(callback).onDataReady(ArgumentMatchers.<InputStream>isNull());
  }

  @Test
  public void testDisconnectsUrlOnCleanup() {
    fetcher.loadData(Priority.HIGH, callback);
    fetcher.cleanup();

    verify(urlConnection).disconnect();
  }

  @Test
  public void testDoesNotThrowIfCleanupCalledBeforeStarted() {
    fetcher.cleanup();
  }

  @Test
  public void testDoesNotThrowIfCancelCalledBeforeStart() {
    fetcher.cancel();
  }

  @Test
  public void testCancelDoesNotDisconnectIfAlreadyConnected() {
    fetcher.loadData(Priority.HIGH, callback);
    fetcher.cancel();

    verify(urlConnection, never()).disconnect();
  }

  @Test
  public void testClosesStreamInCleanupIfNotNull() throws IOException {
    fetcher.loadData(Priority.HIGH, callback);
    fetcher.cleanup();

    verify(stream).close();
  }

  @Test
  public void testClosesStreamBeforeDisconnectingConnection() throws IOException {
    fetcher.loadData(Priority.NORMAL, callback);
    fetcher.cleanup();

    InOrder order = inOrder(stream, urlConnection);
    order.verify(stream).close();
    order.verify(urlConnection).disconnect();
  }
}