Back to Repositories

Testing Dark Mode Resource Loading in bumptech/glide

The DarkModeTest suite validates Glide’s image loading behavior in dark and light mode contexts on Android. It ensures proper drawable resource handling and theme-based color adaptations for various loading scenarios and configurations.

Test Coverage Overview

Comprehensive testing of Glide’s dark mode functionality across different contexts:
  • Activity and Fragment-based image loading
  • Resource URI handling (name and ID based)
  • Placeholder, error, and fallback drawable behaviors
  • Theme inheritance and application
  • Memory cache interactions

Implementation Analysis

The test suite employs JUnit and AndroidX test frameworks to validate dark mode behavior. It uses ActivityScenario for lifecycle management and implements systematic verification of drawable resources through bitmap comparisons. The testing approach covers both direct resource loading and URI-based resource access patterns.

Technical Details

Key technical components:
  • AndroidJUnit4 test runner
  • IdlingGlideRule for async operation handling
  • Espresso for UI synchronization
  • BitmapSubject for image comparison
  • Custom ActivityScenario implementations for dark/light mode forcing

Best Practices Demonstrated

The test suite showcases several testing best practices:
  • Systematic resource cleanup and scenario management
  • Proper activity and fragment lifecycle handling
  • Comprehensive edge case coverage
  • Efficient test setup and teardown patterns
  • Clear separation of test scenarios

bumptech/glide

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

            
package com.bumptech.glide;

import static androidx.test.espresso.Espresso.onIdle;
import static com.bumptech.glide.testutil.BitmapSubject.assertThat;
import static org.junit.Assume.assumeTrue;

import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.bumptech.glide.instrumentation.R;
import com.bumptech.glide.load.engine.executor.IdlingGlideRule;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.test.ForceDarkOrLightModeActivity;
import com.google.common.base.Function;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

@RunWith(AndroidJUnit4.class)
public class DarkModeTest {
  private final Context context = ApplicationProvider.getApplicationContext();

  @Rule
  public final IdlingGlideRule idlingGlideRule =
      IdlingGlideRule.newGlideRule(glideBuilder -> glideBuilder);

  @Before
  public void before() {
    // Dark mode wasn't supported prior to Q.
    assumeTrue(VERSION.SDK_INT >= VERSION_CODES.Q);
  }

  @Test
  public void load_withDarkModeActivity_vectorDrawable_usesDarkModeColor() {
    runActivityDrawableTest(
        darkModeActivity(),
        R.drawable.vector_drawable_dark,
        activity ->
            Glide.with(activity).load(R.drawable.vector_drawable).override(Target.SIZE_ORIGINAL));
  }

  @Test
  public void load_withLightModeActivity_vectorDrawable_usesLightModeColor() {
    runActivityDrawableTest(
        lightModeActivity(),
        R.drawable.vector_drawable_light,
        activity ->
            Glide.with(activity).load(R.drawable.vector_drawable).override(Target.SIZE_ORIGINAL));
  }

  private void runActivityDrawableTest(
      ActivityScenario<? extends FragmentActivity> scenario,
      int expectedResource,
      Function<FragmentActivity, RequestBuilder<Drawable>> glideBuilder) {
    AtomicReference<Bitmap> result = new AtomicReference<>();
    try (scenario) {
      scenario.onActivity(
          activity -> {
            ViewGroup container = findContainer(activity);
            ImageView imageView = newFixedSizeImageView(activity);
            container.addView(imageView);

            glideBuilder.apply(activity).into(imageView);
          });

      // This two step process is because setting the Drawable on the ImageView modifies the
      // drawable in a subsequent frame. If we want our Drawables to produce identical Bitmaps when
      // drawn to a canvas, we need to set both on the ImageView for at least one frame.
      onIdle();
      scenario.onActivity(
          activity -> {
            ImageView imageView = findImageView(activity);
            result.set(drawableToBitmap(imageView.getDrawable()));
            Drawable expectedDrawable = AppCompatResources.getDrawable(activity, expectedResource);
            imageView.setImageDrawable(expectedDrawable);
          });
      onIdle();
      scenario.onActivity(
          activity -> {
            ImageView imageView = findImageView(activity);
            Bitmap expected = drawableToBitmap(imageView.getDrawable());
            assertThat(result.get()).sameAs(expected);
          });
    }
  }

  private static Bitmap drawableToBitmap(Drawable drawable) {
    int width = drawable.getIntrinsicWidth();
    int height = drawable.getIntrinsicHeight();

    Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(result);
    drawable.setBounds(0, 0, width, height);
    drawable.draw(canvas);
    canvas.setBitmap(null);
    return result;
  }

  @Test
  public void load_withDarkModeActivity_useDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
  }

  @Test
  public void load_withDarkModeActivity_afterLoadingWithLightModeActivity_useDarkModeDrawable() {
    // Load with light mode first.
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));

    // Then again with dark mode to make sure that we do not use the cached resource from the
    // previous load.
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
  }

  @Test
  public void
      load_withDarkModeActivity_afterLoadingWithLightModeActivity_memoryCacheCleared_useDarkModeDrawable() {
    // Load with light mode first.
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));

    // Then again with dark mode to make sure that we do not use the cached resource from the
    // previous load.
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity -> {
          Glide.get(context).clearMemory();
          return Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL);
        });
  }

  @Test
  public void load_withDarkModeFragment_usesDarkModeDrawable() {
    runFragmentTest(
        darkModeActivity(),
        R.raw.dog_dark,
        fragment -> Glide.with(fragment).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
  }

  @Test
  public void load_withLightModeActivity_usesLightModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        activity -> Glide.with(activity).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
  }

  @Test
  public void load_withLightModeFragment_usesLightModeDrawable() {
    runFragmentTest(
        lightModeActivity(),
        R.raw.dog_light,
        fragment -> Glide.with(fragment).load(R.drawable.dog).override(Target.SIZE_ORIGINAL));
  }

  @Test
  public void load_withDarkModeActivity_darkModeTheme_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity ->
            Glide.with(activity)
                .load(R.drawable.dog)
                .override(Target.SIZE_ORIGINAL)
                .theme(activity.getTheme()));
  }

  @Test
  public void loadResourceNameUri_withDarkModeActivity_darkModeTheme_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity ->
            Glide.with(activity)
                .load(newResourceNameUri(activity, R.drawable.dog))
                .override(Target.SIZE_ORIGINAL)
                .theme(activity.getTheme()));
  }

  @Test
  public void loadResourceNameUri_withDarkModeActivity_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity ->
            Glide.with(activity)
                .load(newResourceNameUri(activity, R.drawable.dog))
                .override(Target.SIZE_ORIGINAL));
  }

  @Test
  public void
      loadResourceNameUri_withDarkModeActivity_afterLightModeActivity_usesDarkModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        activity ->
            Glide.with(activity)
                .load(newResourceNameUri(activity, R.drawable.dog))
                .override(Target.SIZE_ORIGINAL));
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity ->
            Glide.with(activity)
                .load(newResourceNameUri(activity, R.drawable.dog))
                .override(Target.SIZE_ORIGINAL));
  }

  @Test
  public void loadResourceIdUri_withDarkModeActivity_darkModeTheme_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity ->
            Glide.with(activity)
                .load(newResourceIdUri(activity, R.drawable.dog))
                .override(Target.SIZE_ORIGINAL)
                .theme(activity.getTheme()));
  }

  @Test
  public void loadResourceIdUri_withDarkModeActivity_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        activity ->
            Glide.with(activity)
                .load(newResourceIdUri(activity, R.drawable.dog))
                .override(Target.SIZE_ORIGINAL));
  }

  private static Uri newResourceNameUri(Context context, int resourceId) {
    Resources resources = context.getResources();
    return newResourceUriBuilder(context)
        .appendPath(resources.getResourceTypeName(resourceId))
        .appendPath(resources.getResourceEntryName(resourceId))
        .build();
  }

  private static Uri newResourceIdUri(Context context, int resourceId) {
    return newResourceUriBuilder(context).appendPath(String.valueOf(resourceId)).build();
  }

  private static Uri.Builder newResourceUriBuilder(Context context) {
    return new Uri.Builder()
        .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
        .authority(context.getPackageName());
  }

  @Test
  public void load_withDarkModeFragment_darkModeTheme_usesDarkModeDrawable() {
    runFragmentTest(
        darkModeActivity(),
        R.raw.dog_dark,
        fragment ->
            Glide.with(fragment)
                .load(R.drawable.dog)
                .override(Target.SIZE_ORIGINAL)
                .theme(fragment.requireActivity().getTheme()));
  }

  @Test
  public void loadResourceNameUri_withDarkModeFragment_darkModeTheme_usesDarkModeDrawable() {
    runFragmentTest(
        darkModeActivity(),
        R.raw.dog_dark,
        fragment ->
            Glide.with(fragment)
                .load(newResourceNameUri(fragment.requireContext(), R.drawable.dog))
                .override(Target.SIZE_ORIGINAL)
                .theme(fragment.requireActivity().getTheme()));
  }

  @Test
  public void loadResourceIdUri_withDarkModeFragment_darkModeTheme_usesDarkModeDrawable() {
    runFragmentTest(
        darkModeActivity(),
        R.raw.dog_dark,
        fragment ->
            Glide.with(fragment)
                .load(newResourceIdUri(fragment.requireContext(), R.drawable.dog))
                .override(Target.SIZE_ORIGINAL)
                .theme(fragment.requireActivity().getTheme()));
  }

  @Test
  public void load_withApplicationContext_darkTheme_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input ->
            Glide.with(input.getApplicationContext())
                .load(R.drawable.dog)
                .override(Target.SIZE_ORIGINAL)
                .theme(input.getTheme()));
  }

  @Ignore("TODO(#3751): Consider how to deal with themes applied for application context loads.")
  @Test
  public void load_withApplicationContext_lightTheme_thenDarkTheme_usesDarkModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        input ->
            Glide.with(input.getApplicationContext())
                .load(R.drawable.dog)
                .override(Target.SIZE_ORIGINAL)
                .theme(input.getTheme()));

    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input ->
            Glide.with(input.getApplicationContext())
                .load(R.drawable.dog)
                .override(Target.SIZE_ORIGINAL)
                .theme(input.getTheme()));
  }

  @Test
  public void loadResourceNameUri_withApplicationContext_darkTheme_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input ->
            Glide.with(input.getApplicationContext())
                .load(newResourceNameUri(input.getApplicationContext(), R.drawable.dog))
                .override(Target.SIZE_ORIGINAL)
                .theme(input.getTheme()));
  }

  @Ignore("TODO(#3751): Consider how to deal with themes applied for application context loads.")
  @Test
  public void
      loadResourceNameUri_withApplicationContext_darkTheme_afterLightTheme_usesDarkModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        input ->
            Glide.with(input.getApplicationContext())
                .load(newResourceNameUri(input.getApplicationContext(), R.drawable.dog))
                .override(Target.SIZE_ORIGINAL)
                .theme(input.getTheme()));

    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input ->
            Glide.with(input.getApplicationContext())
                .load(newResourceNameUri(input.getApplicationContext(), R.drawable.dog))
                .override(Target.SIZE_ORIGINAL)
                .theme(input.getTheme()));
  }

  @Test
  public void loadResourceIdUri_withApplicationContext_darkTheme_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input ->
            Glide.with(input.getApplicationContext())
                .load(newResourceIdUri(input.getApplicationContext(), R.drawable.dog))
                .override(Target.SIZE_ORIGINAL)
                .theme(input.getTheme()));
  }

  @Test
  public void load_withApplicationContext_lightTheme_usesLightModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        input ->
            Glide.with(input.getApplicationContext())
                .load(R.drawable.dog)
                .override(Target.SIZE_ORIGINAL)
                .theme(input.getTheme()));
  }

  @Test
  public void load_withLightModeActivity_lightModeTheme_usesLightModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        activity ->
            Glide.with(activity)
                .load(R.drawable.dog)
                .override(Target.SIZE_ORIGINAL)
                .theme(activity.getTheme()));
  }

  @Test
  public void placeholder_withDarkModeActivity_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input -> Glide.with(input).load((Object) null).placeholder(R.drawable.dog));
  }

  @Test
  public void placeholder_withDarkModeFragment_usesDarkModeDrawable() {
    runFragmentTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input -> Glide.with(input).load((Object) null).placeholder(R.drawable.dog));
  }

  @Test
  public void error_withDarkModeActivity_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input -> Glide.with(input).load((Object) null).error(R.drawable.dog));
  }

  @Test
  public void error_withDarkModeFragment_usesDarkModeDrawable() {
    runFragmentTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input -> Glide.with(input).load((Object) null).error(R.drawable.dog));
  }

  @Test
  public void fallback_withDarkModeActivity_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input -> Glide.with(input).load((Object) null).fallback(R.drawable.dog));
  }

  @Test
  public void fallback_withDarkModeFragment_usesDarkModeDrawable() {
    runFragmentTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input -> Glide.with(input).load((Object) null).fallback(R.drawable.dog));
  }

  @Test
  public void placeholder_withLightModeActivity_usesLightModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        input -> Glide.with(input).load((Object) null).placeholder(R.drawable.dog));
  }

  @Test
  public void placeholder_withLightModeFragment_usesLightModeDrawable() {
    runFragmentTest(
        lightModeActivity(),
        R.raw.dog_light,
        input -> Glide.with(input).load((Object) null).placeholder(R.drawable.dog));
  }

  @Test
  public void placeholder_withDarkModeActivityAndTheme_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input ->
            Glide.with(input)
                .load((Object) null)
                .theme(input.getTheme())
                .placeholder(R.drawable.dog));
  }

  @Test
  public void placeholder_withLightModeActivityAndTheme_usesLightModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        input ->
            Glide.with(input)
                .load((Object) null)
                .theme(input.getTheme())
                .placeholder(R.drawable.dog));
  }

  @Test
  public void placeholder_withApplicationContext_darkTheme_usesDarkModeDrawable() {
    runActivityTest(
        darkModeActivity(),
        R.raw.dog_dark,
        input ->
            Glide.with(input.getApplicationContext())
                .load((Object) null)
                .theme(input.getTheme())
                .placeholder(R.drawable.dog));
  }

  @Test
  public void placeholder_withApplicationContext_lightTheme_usesLightModeDrawable() {
    runActivityTest(
        lightModeActivity(),
        R.raw.dog_light,
        input ->
            Glide.with(input.getApplicationContext())
                .load((Object) null)
                .theme(input.getTheme())
                .placeholder(R.drawable.dog));
  }

  private ActivityScenario<FragmentActivity> darkModeActivity() {
    return ActivityScenario.launch(ForceDarkOrLightModeActivity.forceDarkMode(context));
  }

  private ActivityScenario<FragmentActivity> lightModeActivity() {
    return ActivityScenario.launch(ForceDarkOrLightModeActivity.forceLightMode(context));
  }

  private static void runFragmentTest(
      ActivityScenario<? extends FragmentActivity> scenario,
      int expectedResource,
      Function<Fragment, RequestBuilder<Drawable>> requestBuilder) {
    try (scenario) {
      scenario.onActivity(
          activity -> {
            ImageViewFragment fragment = new ImageViewFragment();
            activity
                .getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.container, fragment)
                .commitNowAllowingStateLoss();
            ViewGroup container = findContainer(activity);
            ImageView imageView = (ImageView) container.getChildAt(0);

            requestBuilder.apply(fragment).into(imageView);
          });

      assertImageViewContainerChildHasContent(scenario, expectedResource);
    }
  }

  /** Fragment that displays a single fixed size ImageView. */
  public static final class ImageViewFragment extends Fragment {
    @Override
    public View onCreateView(
        @NonNull LayoutInflater inflater,
        @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
      return newFixedSizeImageView(getContext());
    }
  }

  private static ImageView newFixedSizeImageView(Context context) {
    ImageView imageView = new ImageView(context);
    imageView.setLayoutParams(new LayoutParams(200, 200));
    return imageView;
  }

  private static void runActivityTest(
      ActivityScenario<? extends FragmentActivity> scenario,
      int expectedResource,
      Function<FragmentActivity, RequestBuilder<Drawable>> glideBuilder) {
    try (scenario) {
      scenario.onActivity(
          activity -> {
            ViewGroup container = findContainer(activity);
            ImageView imageView = newFixedSizeImageView(activity);
            container.addView(imageView);

            glideBuilder.apply(activity).into(imageView);
          });

      assertImageViewContainerChildHasContent(scenario, expectedResource);
    }
  }

  private static void assertImageViewContainerChildHasContent(
      ActivityScenario<? extends FragmentActivity> scenario, int expectedResource) {
    onIdle();
    scenario.onActivity(
        activity -> {
          ImageView imageView = findImageView(activity);
          Bitmap bitmap = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
          assertThat(bitmap).sameAs(expectedResource);
        });
  }

  private static ImageView findImageView(FragmentActivity activity) {
    ViewGroup container = findContainer(activity);
    return (ImageView) container.getChildAt(0);
  }

  private static ViewGroup findContainer(FragmentActivity activity) {
    return activity.findViewById(R.id.container);
  }
}