Back to Repositories

Testing ListPreloader Scroll Behavior in Glide

This test suite validates the ListPreloader functionality in Glide, focusing on image preloading behavior in scrollable lists. It ensures proper scroll event handling, item position tracking, and resource loading optimization.

Test Coverage Overview

The test suite provides comprehensive coverage of ListPreloader’s core functionality:

  • Scroll direction handling (increasing and decreasing)
  • Boundary condition testing for list positions
  • Multiple item preloading scenarios
  • Prevention of redundant preloading
  • Integration with Glide’s RequestBuilder system

Implementation Analysis

The testing approach employs JUnit with Robolectric for Android environment simulation. It uses mock objects extensively to isolate ListPreloader behavior and verify correct interaction patterns. The tests leverage atomic variables and custom adapters to track method invocations and verify execution order.

Key patterns include callback verification, position tracking, and resource size validation.

Technical Details

Testing infrastructure includes:

  • JUnit 4 test framework
  • Robolectric for Android runtime simulation
  • Mockito for mock object creation and verification
  • Custom ListPreloaderAdapter implementation
  • ArgumentCaptor for detailed interaction verification

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Thorough setup and tear down management
  • Comprehensive edge case coverage
  • Clear test method naming conventions
  • Efficient mock object usage
  • Isolated test scenarios
  • Detailed verification of component interactions

bumptech/glide

library/test/src/test/java/com/bumptech/glide/ListPreloaderTest.java

            
package com.bumptech.glide;

import static com.bumptech.glide.tests.Util.cast;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.request.target.SizeReadyCallback;
import com.bumptech.glide.request.target.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
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.verification.VerificationMode;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK)
public class ListPreloaderTest {

  @Mock private RequestBuilder<Object> request;
  @Mock private RequestManager requestManager;

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

  @Test
  public void testGetItemsIsCalledIncreasing() {
    final AtomicBoolean called = new AtomicBoolean(false);
    final AtomicInteger calledCount = new AtomicInteger();

    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            called.set(true);
            final int count = calledCount.getAndIncrement();
            assertEquals(11 + count, position);
            return super.getPreloadItems(position);
          }
        };
    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);
    preloader.onScroll(null, 1, 10, 30);
    assertEquals(10, calledCount.get());
  }

  @Test
  public void testGetItemsIsCalledInOrderIncreasing() {
    final int toPreload = 10;
    final List<Object> objects = new ArrayList<>();
    for (int i = 0; i < toPreload; i++) {
      objects.add(i);
    }

    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          private int expectedPosition;

          @Override
          public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) {
            return new int[] {10, 10};
          }

          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            return objects.subList(position - 11, position + 1 - 11);
          }

          @Nullable
          @Override
          @SuppressWarnings("unchecked")
          public RequestBuilder<Object> getPreloadRequestBuilder(@NonNull Object item) {
            assertEquals(objects.get(expectedPosition), item);
            expectedPosition++;
            return mock(RequestBuilder.class);
          }
        };
    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, toPreload);
    preloader.onScroll(null, 1, 10, 20);
  }

  @Test
  public void testGetItemsIsCalledDecreasing() {
    final AtomicBoolean called = new AtomicBoolean(false);
    final AtomicInteger calledCount = new AtomicInteger();
    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            // Ignore the preload caused from us starting at the end
            if (position >= 40) {
              return Collections.emptyList();
            }
            final int count = calledCount.getAndIncrement();
            called.set(true);
            assertEquals(28 - count, position);
            return super.getPreloadItems(position);
          }
        };
    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);
    preloader.onScroll(null, 30, 10, 40);
    preloader.onScroll(null, 29, 10, 40);
    assertTrue(called.get());
  }

  @Test
  public void testGetItemsIsCalledInOrderDecreasing() {
    final int toPreload = 10;
    final List<Object> objects = new ArrayList<>();
    for (int i = 0; i < toPreload; i++) {
      objects.add(new Object());
    }

    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          private int expectedPosition = toPreload - 1;

          @Override
          public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) {
            return new int[] {10, 10};
          }

          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            if (position == 40) {
              return Collections.emptyList();
            }
            return objects.subList(position, position + 1);
          }

          @Nullable
          @Override
          @SuppressWarnings("unchecked")
          public RequestBuilder<Object> getPreloadRequestBuilder(@NonNull Object item) {
            assertEquals(objects.get(expectedPosition), item);
            expectedPosition--;
            return mock(RequestBuilder.class);
          }
        };
    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, toPreload);
    preloader.onScroll(null, 30, 10, 10);
    preloader.onScroll(null, 29, 10, 10);
  }

  @Test
  public void testGetItemsIsNeverCalledWithEndGreaterThanTotalItems() {
    final AtomicBoolean called = new AtomicBoolean(false);
    final AtomicInteger calledCount = new AtomicInteger();
    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            called.set(true);
            final int count = calledCount.getAndIncrement();
            assertEquals(26 + count, position);
            return super.getPreloadItems(position);
          }
        };
    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);
    preloader.onScroll(null, 16, 10, 30);
    assertTrue(called.get());
  }

  @Test
  public void testGetItemsIsNeverCalledWithStartLessThanZero() {
    final AtomicBoolean called = new AtomicBoolean(false);
    final AtomicInteger calledCount = new AtomicInteger();
    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            if (position >= 17) {
              return Collections.emptyList();
            }
            called.set(true);
            final int count = calledCount.getAndIncrement();
            assertEquals(5 - count, position);
            return super.getPreloadItems(position);
          }
        };

    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);
    preloader.onScroll(null, 7, 10, 30);
    preloader.onScroll(null, 6, 10, 30);
    assertTrue(called.get());
  }

  @Test
  public void testDontPreloadItemsRepeatedlyWhileIncreasing() {
    final AtomicInteger called = new AtomicInteger();
    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            final int current = called.getAndIncrement();
            assertEquals(11 + current, position);
            return super.getPreloadItems(position);
          }
        };

    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);
    preloader.onScroll(null, 1, 10, 30);
    preloader.onScroll(null, 4, 10, 30);

    assertEquals(13, called.get());
  }

  @Test
  public void testDontPreloadItemsRepeatedlyWhileDecreasing() {
    final AtomicInteger called = new AtomicInteger();
    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            if (position >= 20) {
              return Collections.emptyList();
            }
            final int current = called.getAndIncrement();
            assertEquals(19 - current, position);
            return super.getPreloadItems(position);
          }
        };

    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);
    preloader.onScroll(null, 21, 10, 30);
    preloader.onScroll(null, 20, 10, 30);
    preloader.onScroll(null, 17, 10, 30);
    assertEquals(13, called.get());
  }

  @Test
  public void testMultipleItemsForPositionIncreasing() {
    final List<Object> objects = new ArrayList<>();
    objects.add(new Object());
    objects.add(new Object());
    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          private int expectedPosition = (1 + 10) * 2;

          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            return objects;
          }

          @Override
          public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) {
            assertEquals(expectedPosition / 2, adapterPosition);
            assertEquals(expectedPosition % 2, itemPosition);
            expectedPosition++;
            return itemPosition == 0 ? new int[] {10, 11} : new int[] {20, 21};
          }

          @Nullable
          @Override
          public RequestBuilder<Object> getPreloadRequestBuilder(@NonNull Object item) {
            return request;
          }
        };
    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);
    Iterable<Integer> expected = Arrays.asList(10, 11, 20, 21, 10, 11, 20, 21);

    preloader.onScroll(null, 1, 10, 1 + 10 + 2);

    List<Integer> allValues = getTargetsSizes(request, times(4));
    assertEquals(expected, allValues);
  }

  @Test
  public void testMultipleItemsForPositionDecreasing() {
    final List<Object> objects = new ArrayList<>();
    objects.add(new Object());
    objects.add(new Object());
    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          private int expectedPosition = objects.size() * 2 - 1;

          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            return objects;
          }

          @Override
          public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) {
            assertEquals(expectedPosition / 2, adapterPosition);
            assertEquals(expectedPosition % 2, itemPosition);
            expectedPosition--;
            return itemPosition == 0 ? new int[] {10, 11} : new int[] {20, 21};
          }

          @Nullable
          @Override
          public RequestBuilder<Object> getPreloadRequestBuilder(@NonNull Object item) {
            return request;
          }
        };
    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);
    Iterable<Integer> expected = Arrays.asList(20, 21, 10, 11, 20, 21, 10, 11);

    preloader.onScroll(null, 3, 2, 3 + 2);
    preloader.onScroll(null, 2, 2, 3 + 2);

    List<Integer> allValues = getTargetsSizes(request, times(4));
    assertEquals(expected, allValues);
  }

  private <Resource> List<Integer> getTargetsSizes(
      RequestBuilder<Resource> requestBuilder, VerificationMode mode) {
    ArgumentCaptor<Integer> integerArgumentCaptor = ArgumentCaptor.forClass(Integer.class);
    ArgumentCaptor<Target<Resource>> targetArgumentCaptor =
        cast(ArgumentCaptor.forClass(Target.class));
    SizeReadyCallback cb = mock(SizeReadyCallback.class);
    verify(requestBuilder, mode).into(targetArgumentCaptor.capture());
    for (Target<Resource> target : targetArgumentCaptor.getAllValues()) {
      target.getSize(cb);
    }
    verify(cb, mode).onSizeReady(integerArgumentCaptor.capture(), integerArgumentCaptor.capture());
    return integerArgumentCaptor.getAllValues();
  }

  // It's safe to ignore the return value of containsAllIn.
  @SuppressWarnings("ResultOfMethodCallIgnored")
  @Test
  public void testItemsArePreloadedWithGlide() {
    final List<Object> objects = new ArrayList<>();
    objects.add(new Object());
    objects.add(new Object());
    final HashSet<Object> loadedObjects = new HashSet<>();
    ListPreloaderAdapter preloaderAdapter =
        new ListPreloaderAdapter() {
          @NonNull
          @Override
          public List<Object> getPreloadItems(int position) {
            return objects.subList(position - 11, position - 10);
          }

          @Nullable
          @Override
          public RequestBuilder<Object> getPreloadRequestBuilder(@NonNull Object item) {
            loadedObjects.add(item);
            return super.getPreloadRequestBuilder(item);
          }
        };
    ListPreloader<Object> preloader =
        new ListPreloader<>(requestManager, preloaderAdapter, preloaderAdapter, 10);

    preloader.onScroll(null, 1, 10, 13);
    assertThat(loadedObjects).containsAtLeastElementsIn(objects);
  }

  private static class ListPreloaderAdapter
      implements ListPreloader.PreloadModelProvider<Object>,
          ListPreloader.PreloadSizeProvider<Object> {

    public ListPreloaderAdapter() {}

    @NonNull
    @Override
    public List<Object> getPreloadItems(int position) {
      ArrayList<Object> result = new ArrayList<>(1);
      Collections.fill(result, new Object());
      return result;
    }

    @Nullable
    @Override
    @SuppressWarnings("unchecked")
    public RequestBuilder<Object> getPreloadRequestBuilder(@NonNull Object item) {
      return mock(RequestBuilder.class);
    }

    @Nullable
    @Override
    public int[] getPreloadSize(@NonNull Object item, int adapterPosition, int itemPosition) {
      return new int[] {100, 100};
    }
  }
}