Back to Repositories

Testing Drawable Transformation Implementation in Glide Library

This test suite validates the DrawableTransformation class in Glide, focusing on bitmap drawable transformations and resource management. It ensures proper handling of bitmap recycling and transformation behavior for different drawable types.

Test Coverage Overview

The test suite provides comprehensive coverage of DrawableTransformation functionality, including:
  • Bitmap drawable transformation scenarios
  • Color drawable transformation handling
  • Resource recycling verification
  • Transformation equality testing
Key edge cases tested include bitmap recycling behavior and transformation chain validation.

Implementation Analysis

The testing approach utilizes Robolectric for Android environment simulation and Mockito for mocking dependencies. The implementation demonstrates sophisticated use of mock answers and verification patterns to ensure proper resource management and transformation behavior.

Notable patterns include custom Answer implementations for bitmap transformation validation and resource handling verification.

Technical Details

Testing tools and configuration:
  • JUnit 4 test framework
  • Robolectric for Android runtime simulation
  • Mockito for mock object creation and verification
  • Custom KeyTester utility for equality testing
  • BitmapPool and Resource management utilities

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Proper test setup and teardown with @Before and @After annotations
  • Comprehensive mock object initialization and verification
  • Thorough equality testing with regression checks
  • Clear test method naming and organization
  • Effective use of assertion utilities for verification

bumptech/glide

library/test/src/test/java/com/bumptech/glide/load/resource/bitmap/DrawableTransformationTest.java

            
package com.bumptech.glide.load.resource.bitmap;

import static com.bumptech.glide.RobolectricConstants.ROBOLECTRIC_SDK;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
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.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import androidx.test.core.app.ApplicationProvider;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPoolAdapter;
import com.bumptech.glide.load.resource.SimpleResource;
import com.bumptech.glide.tests.KeyTester;
import com.bumptech.glide.tests.Util;
import java.security.MessageDigest;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
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;

@RunWith(RobolectricTestRunner.class)
@Config(sdk = ROBOLECTRIC_SDK)
public class DrawableTransformationTest {
  @Rule public final KeyTester keyTester = new KeyTester();
  @Mock private Transformation<Bitmap> bitmapTransformation;
  private BitmapPool bitmapPool;
  private DrawableTransformation transformation;
  private Context context;

  @Before
  public void setUp() {
    MockitoAnnotations.initMocks(this);
    transformation = new DrawableTransformation(bitmapTransformation, /* isRequired= */ true);
    context = ApplicationProvider.getApplicationContext();
    bitmapPool = new BitmapPoolAdapter();
    Glide.init(context, new GlideBuilder().setBitmapPool(bitmapPool));
  }

  @After
  public void tearDown() {
    Glide.tearDown();
  }

  @Test
  public void transform_withBitmapDrawable_andUnitBitmapTransformation_doesNotRecycle() {
    when(bitmapTransformation.transform(
            any(Context.class), anyBitmapResource(), anyInt(), anyInt()))
        .thenAnswer(new ReturnGivenResource());

    Bitmap bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
    BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap);
    @SuppressWarnings("unchecked")
    Resource<Drawable> input =
        (Resource<Drawable>) (Resource<?>) new BitmapDrawableResource(drawable, bitmapPool);
    transformation.transform(context, input, /* outWidth= */ 100, /* outHeight= */ 200);

    assertThat(bitmap.isRecycled()).isFalse();
  }

  @Test
  public void transform_withBitmapDrawable_andFunctionalBitmapTransformation_doesNotRecycle() {
    when(bitmapTransformation.transform(
            any(Context.class), anyBitmapResource(), anyInt(), anyInt()))
        .thenAnswer(
            new Answer<Resource<Bitmap>>() {
              @Override
              public Resource<Bitmap> answer(InvocationOnMock invocationOnMock) throws Throwable {
                return BitmapResource.obtain(
                    Bitmap.createBitmap(200, 200, Bitmap.Config.ARGB_8888), bitmapPool);
              }
            });
    Bitmap bitmap = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888);
    BitmapDrawable drawable = new BitmapDrawable(context.getResources(), bitmap);
    @SuppressWarnings("unchecked")
    Resource<Drawable> input =
        (Resource<Drawable>) (Resource<?>) new BitmapDrawableResource(drawable, bitmapPool);
    transformation.transform(context, input, /* outWidth= */ 100, /* outHeight= */ 200);

    assertThat(bitmap.isRecycled()).isFalse();
  }

  @Test
  public void transform_withColorDrawable_andUnitBitmapTransformation_recycles() {
    bitmapPool = mock(BitmapPool.class);
    Glide.tearDown();
    Glide.init(context, new GlideBuilder().setBitmapPool(bitmapPool));
    when(bitmapTransformation.transform(
            any(Context.class), anyBitmapResource(), anyInt(), anyInt()))
        .thenAnswer(new ReturnGivenResource());

    ColorDrawable colorDrawable = new ColorDrawable(Color.RED);
    final Resource<Drawable> input = new SimpleResource<Drawable>(colorDrawable);

    doAnswer(
            new Answer<Void>() {
              @Override
              public Void answer(InvocationOnMock invocationOnMock) throws Throwable {
                Bitmap bitmap = (Bitmap) invocationOnMock.getArguments()[0];
                assertThat(bitmap.getWidth()).isEqualTo(100);
                assertThat(bitmap.getHeight()).isEqualTo(200);
                return null;
              }
            })
        .when(bitmapPool)
        .put(any(Bitmap.class));
    when(bitmapPool.get(anyInt(), anyInt(), any(Bitmap.Config.class)))
        .thenAnswer(
            new Answer<Bitmap>() {
              @Override
              public Bitmap answer(InvocationOnMock invocationOnMock) throws Throwable {
                int width = (Integer) invocationOnMock.getArguments()[0];
                int height = (Integer) invocationOnMock.getArguments()[1];
                Bitmap.Config config = (Bitmap.Config) invocationOnMock.getArguments()[2];
                return Bitmap.createBitmap(width, height, config);
              }
            });

    transformation.transform(context, input, /* outWidth= */ 100, /* outHeight= */ 200);

    verify(bitmapPool).put(isA(Bitmap.class));
  }

  @Test
  public void testEquals() {
    BitmapTransformation otherBitmapTransformation = mock(BitmapTransformation.class);
    doAnswer(new Util.WriteDigest("bitmapTransformation"))
        .when(bitmapTransformation)
        .updateDiskCacheKey(any(MessageDigest.class));
    doAnswer(new Util.WriteDigest("otherBitmapTransformation"))
        .when(otherBitmapTransformation)
        .updateDiskCacheKey(any(MessageDigest.class));

    keyTester
        .addEquivalenceGroup(
            transformation,
            new DrawableTransformation(bitmapTransformation, /* isRequired= */ true),
            new DrawableTransformation(bitmapTransformation, /* isRequired= */ false))
        .addEquivalenceGroup(bitmapTransformation)
        .addEquivalenceGroup(otherBitmapTransformation)
        .addEquivalenceGroup(
            new DrawableTransformation(otherBitmapTransformation, /* isRequired= */ true),
            new DrawableTransformation(otherBitmapTransformation, /* isRequired= */ false))
        .addRegressionTest(
            new DrawableTransformation(bitmapTransformation, /* isRequired= */ true),
            "eddf60c557a6315a489b8a3a19b12439a90381256289fbe9a503afa726230bd9")
        .addRegressionTest(
            new DrawableTransformation(otherBitmapTransformation, /* isRequired= */ false),
            "40931536ed0ec97c39d4be10c44f5b69a86030ec575317f5a0f17e15a0ea9be8")
        .test();
  }

  @SuppressWarnings("unchecked")
  private static Resource<Bitmap> anyBitmapResource() {
    return any(Resource.class);
  }

  private static final class ReturnGivenResource implements Answer<Resource<Bitmap>> {

    @Override
    public Resource<Bitmap> answer(InvocationOnMock invocationOnMock) throws Throwable {
      @SuppressWarnings("unchecked")
      Resource<Bitmap> input = (Resource<Bitmap>) invocationOnMock.getArguments()[1];
      return input;
    }
  }
}