Back to Repositories

Testing Byte Array Loading and Caching Workflows in Glide

A comprehensive test suite for validating byte array loading and caching behavior in Glide’s image loading library. These tests verify proper handling of image data through memory and disk caches while ensuring correct bitmap loading from byte arrays.

Test Coverage Overview

The test suite provides extensive coverage of Glide’s byte array loading capabilities, focusing on:
  • Loading different byte arrays into ImageViews
  • Memory cache validation and behavior
  • Disk cache strategy verification
  • Resource loading from various sources (local, memory cache, disk cache)
  • Cache strategy configuration persistence

Implementation Analysis

The testing approach utilizes JUnit4 with AndroidJUnit4 runner for Android-specific testing. Tests employ mock objects and concurrency helpers to ensure reliable asynchronous operations. The implementation validates both RequestManager and RequestBuilder workflows, with particular attention to caching behaviors and image loading sources.

Technical Details

Key technical components include:
  • MockitoAnnotations for mock object creation
  • ConcurrencyHelper for managing async operations
  • Custom GlideExecutor configuration
  • BitmapFactory for image processing
  • Various DiskCacheStrategy implementations
  • RequestListener for tracking load sources

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Proper test isolation using @Before setup
  • Comprehensive verification of cache behaviors
  • Explicit memory management
  • Defensive copying of bitmaps
  • Thorough source verification using RequestListener
  • Clear test method naming conventions

bumptech/glide

instrumentation/src/androidTest/java/com/bumptech/glide/LoadBytesTest.java

            
package com.bumptech.glide;

import static com.bumptech.glide.test.GlideOptions.skipMemoryCacheOf;
import static com.bumptech.glide.testutil.BitmapSubject.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.verify;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.widget.AbsListView.LayoutParams;
import android.widget.ImageView;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.executor.GlideExecutor;
import com.bumptech.glide.load.engine.executor.MockGlideExecutor;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.test.GlideApp;
import com.bumptech.glide.test.ResourceIds;
import com.bumptech.glide.testutil.ConcurrencyHelper;
import com.bumptech.glide.testutil.TearDownGlide;
import com.google.common.io.ByteStreams;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

@RunWith(AndroidJUnit4.class)
public class LoadBytesTest {
  @Rule public final TearDownGlide tearDownGlide = new TearDownGlide();
  private final ConcurrencyHelper concurrency = new ConcurrencyHelper();

  @Mock private RequestListener<Drawable> requestListener;

  private Context context;
  private ImageView imageView;

  @Before
  public void setUp() throws IOException {
    MockitoAnnotations.initMocks(this);
    context = ApplicationProvider.getApplicationContext();

    imageView = new ImageView(context);
    int[] dimensions = getCanonicalDimensions();
    imageView.setLayoutParams(new LayoutParams(/* w= */ dimensions[0], /* h= */ dimensions[1]));

    // Writes to the resource disk cache run in a non-blocking manner after the Target is notified.
    // Unless we enforce a single threaded executor, the encode task races with our second decode
    // task, causing the test to sometimes fail (when the second resource is started after the
    // encode and loaded from the disk cache) and sometimes succeed (when the second resource is
    // started before the encode and loads from source).
    ExecutorService executor = Executors.newSingleThreadExecutor();
    GlideExecutor glideExecutor = MockGlideExecutor.newTestExecutor(executor);
    Glide.init(
        context,
        new GlideBuilder()
            .setAnimationExecutor(glideExecutor)
            .setDiskCacheExecutor(glideExecutor)
            .setSourceExecutor(glideExecutor));
  }

  @Test
  public void loadFromRequestManager_intoImageView_withDifferentByteArrays_loadsDifferentImages()
      throws IOException {
    final byte[] canonicalBytes = getCanonicalBytes();
    final byte[] modifiedBytes = getModifiedBytes();

    concurrency.loadOnMainThread(Glide.with(context).load(canonicalBytes), imageView);
    Bitmap firstBitmap = copyFromImageViewDrawable(imageView);

    concurrency.loadOnMainThread(Glide.with(context).load(modifiedBytes), imageView);
    Bitmap secondBitmap = copyFromImageViewDrawable(imageView);

    // This assertion alone doesn't catch the case where the second Bitmap is loaded from the result
    // cache of the data from the first Bitmap.
    assertThat(firstBitmap).isNotSameInstanceAs(secondBitmap);

    Bitmap expectedCanonicalBitmap =
        BitmapFactory.decodeByteArray(canonicalBytes, /* offset= */ 0, canonicalBytes.length);
    assertThat(firstBitmap).sameAs(expectedCanonicalBitmap);

    Bitmap expectedModifiedBitmap =
        BitmapFactory.decodeByteArray(modifiedBytes, /* offset= */ 0, modifiedBytes.length);
    assertThat(secondBitmap).sameAs(expectedModifiedBitmap);
  }

  @Test
  public void loadFromRequestBuilder_intoImageView_withDifferentByteArrays_loadsDifferentImages()
      throws IOException {
    final byte[] canonicalBytes = getCanonicalBytes();
    final byte[] modifiedBytes = getModifiedBytes();

    concurrency.loadOnMainThread(
        GlideApp.with(context).asDrawable().load(canonicalBytes), imageView);
    Bitmap firstBitmap = copyFromImageViewDrawable(imageView);

    concurrency.loadOnMainThread(
        GlideApp.with(context).asDrawable().load(modifiedBytes), imageView);
    Bitmap secondBitmap = copyFromImageViewDrawable(imageView);

    // This assertion alone doesn't catch the case where the second Bitmap is loaded from the result
    // cache of the data from the first Bitmap.
    assertThat(firstBitmap).isNotSameInstanceAs(secondBitmap);

    Bitmap expectedCanonicalBitmap =
        BitmapFactory.decodeByteArray(canonicalBytes, /* offset= */ 0, canonicalBytes.length);
    assertThat(firstBitmap).sameAs(expectedCanonicalBitmap);

    Bitmap expectedModifiedBitmap =
        BitmapFactory.decodeByteArray(modifiedBytes, /* offset= */ 0, modifiedBytes.length);
    assertThat(secondBitmap).sameAs(expectedModifiedBitmap);
  }

  @Test
  public void requestManager_intoImageView_withSameByteArrayAndMemoryCacheEnabled_loadsFromMemory()
      throws IOException {
    final byte[] canonicalBytes = getCanonicalBytes();
    concurrency.loadOnMainThread(
        Glide.with(context).load(canonicalBytes).apply(skipMemoryCacheOf(false)), imageView);

    Glide.with(context).clear(imageView);

    concurrency.loadOnMainThread(
        Glide.with(context)
            .load(canonicalBytes)
            .listener(requestListener)
            .apply(skipMemoryCacheOf(false)),
        imageView);

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.MEMORY_CACHE),
            anyBoolean());
  }

  @Test
  public void requestBuilder_intoImageView_withSameByteArrayAndMemoryCacheEnabled_loadsFromMemory()
      throws IOException {
    final byte[] canonicalBytes = getCanonicalBytes();
    concurrency.loadOnMainThread(
        Glide.with(context).asDrawable().load(canonicalBytes).apply(skipMemoryCacheOf(false)),
        imageView);

    Glide.with(context).clear(imageView);

    concurrency.loadOnMainThread(
        Glide.with(context)
            .asDrawable()
            .load(canonicalBytes)
            .listener(requestListener)
            .apply(skipMemoryCacheOf(false)),
        imageView);

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.MEMORY_CACHE),
            anyBoolean());
  }

  @Test
  public void loadFromRequestManager_withSameByteArray_validDiskCacheStrategy_returnsFromDiskCache()
      throws IOException {
    byte[] data = getCanonicalBytes();
    Target<Drawable> target =
        concurrency.wait(
            GlideApp.with(context)
                .load(data)
                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
                .submit());
    GlideApp.with(context).clear(target);

    concurrency.runOnMainThread(
        new Runnable() {
          @Override
          public void run() {
            GlideApp.get(context).clearMemory();
          }
        });

    concurrency.wait(
        GlideApp.with(context)
            .load(data)
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .listener(requestListener)
            .submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.RESOURCE_DISK_CACHE),
            anyBoolean());
  }

  @Test
  public void loadFromRequestBuilder_withSameByteArray_validDiskCacheStrategy_returnsFromDiskCache()
      throws IOException {
    byte[] data = getCanonicalBytes();
    Target<Drawable> target =
        concurrency.wait(
            GlideApp.with(context)
                .asDrawable()
                .load(data)
                .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
                .submit());
    GlideApp.with(context).clear(target);

    concurrency.runOnMainThread(
        new Runnable() {
          @Override
          public void run() {
            GlideApp.get(context).clearMemory();
          }
        });

    concurrency.wait(
        GlideApp.with(context)
            .asDrawable()
            .load(data)
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .listener(requestListener)
            .submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.RESOURCE_DISK_CACHE),
            anyBoolean());
  }

  @Test
  public void loadFromRequestManager_withSameByteArray_memoryCacheEnabled_returnsFromCache()
      throws IOException {
    byte[] data = getCanonicalBytes();
    Target<Drawable> target =
        concurrency.wait(GlideApp.with(context).load(data).skipMemoryCache(false).submit());
    GlideApp.with(context).clear(target);

    concurrency.wait(
        GlideApp.with(context)
            .load(data)
            .skipMemoryCache(false)
            .listener(requestListener)
            .submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.MEMORY_CACHE),
            anyBoolean());
  }

  @Test
  public void loadFromRequestBuilder_withSameByteArray_memoryCacheEnabled_returnsFromCache()
      throws IOException {
    byte[] data = getCanonicalBytes();
    Target<Drawable> target =
        concurrency.wait(
            GlideApp.with(context).asDrawable().load(data).skipMemoryCache(false).submit());
    GlideApp.with(context).clear(target);

    concurrency.wait(
        GlideApp.with(context)
            .asDrawable()
            .load(data)
            .skipMemoryCache(false)
            .listener(requestListener)
            .submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.MEMORY_CACHE),
            anyBoolean());
  }

  @Test
  public void loadFromRequestManager_withSameByteArray_returnsFromLocal() throws IOException {
    byte[] data = getCanonicalBytes();
    Target<Drawable> target = concurrency.wait(GlideApp.with(context).load(data).submit());
    GlideApp.with(context).clear(target);

    concurrency.wait(GlideApp.with(context).load(data).listener(requestListener).submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.LOCAL),
            anyBoolean());
  }

  @Test
  public void loadFromRequestBuilder_withSameByteArray_returnsFromLocal() throws IOException {
    byte[] data = getCanonicalBytes();
    Target<Drawable> target =
        concurrency.wait(GlideApp.with(context).asDrawable().load(data).submit());
    GlideApp.with(context).clear(target);

    concurrency.wait(
        GlideApp.with(context).asDrawable().load(data).listener(requestListener).submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.LOCAL),
            anyBoolean());
  }

  @Test
  public void loadFromRequestManager_withSameByteArrayAndMissingFromMemory_returnsFromLocal()
      throws IOException {
    byte[] data = getCanonicalBytes();
    Target<Drawable> target = concurrency.wait(GlideApp.with(context).load(data).submit());
    GlideApp.with(context).clear(target);

    concurrency.runOnMainThread(
        new Runnable() {
          @Override
          public void run() {
            GlideApp.get(context).clearMemory();
          }
        });

    concurrency.wait(GlideApp.with(context).load(data).listener(requestListener).submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.LOCAL),
            anyBoolean());
  }

  @Test
  public void loadFromRequestBuilder_withSameByteArrayAndMissingFromMemory_returnsFromLocal()
      throws IOException {
    byte[] data = getCanonicalBytes();
    Target<Drawable> target =
        concurrency.wait(GlideApp.with(context).asDrawable().load(data).submit());
    GlideApp.with(context).clear(target);

    concurrency.runOnMainThread(
        new Runnable() {
          @Override
          public void run() {
            GlideApp.get(context).clearMemory();
          }
        });

    concurrency.wait(
        GlideApp.with(context).asDrawable().load(data).listener(requestListener).submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.LOCAL),
            anyBoolean());
  }

  @Test
  public void loadFromBuilder_withDiskCacheStrategySetBeforeLoad_doesNotOverrideDiskCacheStrategy()
      throws IOException {
    byte[] data = getCanonicalBytes();
    concurrency.wait(
        GlideApp.with(context)
            .asDrawable()
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .load(data)
            .submit());

    concurrency.runOnMainThread(
        new Runnable() {
          @Override
          public void run() {
            GlideApp.get(context).clearMemory();
          }
        });

    concurrency.wait(
        GlideApp.with(context)
            .asDrawable()
            .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
            .listener(requestListener)
            .load(data)
            .submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.RESOURCE_DISK_CACHE),
            anyBoolean());
  }

  @Test
  public void loadFromBuilder_withSkipMemoryCacheSetBeforeLoad_doesNotOverrideSkipMemoryCache()
      throws IOException {
    byte[] data = getCanonicalBytes();
    concurrency.wait(
        GlideApp.with(context).asDrawable().skipMemoryCache(false).load(data).submit());

    concurrency.runOnMainThread(
        new Runnable() {
          @Override
          public void run() {
            GlideApp.get(context).clearMemory();
          }
        });

    concurrency.wait(
        GlideApp.with(context)
            .asDrawable()
            .skipMemoryCache(false)
            .listener(requestListener)
            .load(data)
            .submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.MEMORY_CACHE),
            anyBoolean());
  }

  @Test
  public void loadFromBuilder_withDataDiskCacheStrategy_returnsFromSource() throws IOException {
    byte[] data = getCanonicalBytes();

    concurrency.wait(
        GlideApp.with(context)
            .asDrawable()
            .diskCacheStrategy(DiskCacheStrategy.DATA)
            .load(data)
            .submit());

    concurrency.wait(
        GlideApp.with(context)
            .asDrawable()
            .diskCacheStrategy(DiskCacheStrategy.DATA)
            .skipMemoryCache(true)
            .load(data)
            .listener(requestListener)
            .submit());

    verify(requestListener)
        .onResourceReady(
            ArgumentMatchers.<Drawable>any(),
            any(),
            ArgumentMatchers.<Target<Drawable>>any(),
            eq(DataSource.DATA_DISK_CACHE),
            anyBoolean());
  }

  private Bitmap copyFromImageViewDrawable(ImageView imageView) {
    if (imageView.getDrawable() == null) {
      fail("Drawable unexpectedly null");
    }

    // Glide mutates Bitmaps, so it's possible that a Bitmap loaded into a View in one place may
    // be re-used to load a different image later. Create a defensive copy just in case.
    return Bitmap.createBitmap(((BitmapDrawable) imageView.getDrawable()).getBitmap());
  }

  private int[] getCanonicalDimensions() throws IOException {
    byte[] canonicalBytes = getCanonicalBytes();
    Bitmap bitmap =
        BitmapFactory.decodeByteArray(canonicalBytes, /* offset= */ 0, canonicalBytes.length);
    return new int[] {bitmap.getWidth(), bitmap.getHeight()};
  }

  private byte[] getModifiedBytes() throws IOException {
    int[] dimensions = getCanonicalDimensions();
    Bitmap bitmap = Bitmap.createBitmap(dimensions[0], dimensions[1], Config.ARGB_8888);
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    bitmap.compress(CompressFormat.PNG, /* quality= */ 100, os);
    return os.toByteArray();
  }

  private byte[] getCanonicalBytes() throws IOException {
    int resourceId = ResourceIds.raw.canonical;
    Resources resources = context.getResources();
    InputStream is = resources.openRawResource(resourceId);
    return ByteStreams.toByteArray(is);
  }
}