Back to Repositories

Testing CustomViewTarget Size Calculations and Request Handling in Glide

This test suite validates the CustomViewTarget implementation in the Glide image loading library, focusing on view size calculations, request handling, and lifecycle management. The tests ensure proper behavior for view attachment states, dimension calculations, and request clearing functionality.

Test Coverage Overview

The test suite provides comprehensive coverage of CustomViewTarget functionality including:
  • View size determination and callback handling
  • Layout parameter processing and dimension calculations
  • Request lifecycle management (setting, clearing, retrieving)
  • View attachment state handling
  • Padding and layout dimension edge cases

Implementation Analysis

The testing approach utilizes Robolectric for Android environment simulation and Mockito for mocking dependencies. The tests follow a systematic pattern of setting up view hierarchies, manipulating layouts and dimensions, and verifying correct size callback and request handling behavior.

Key patterns include pre/post layout testing, view tree observer verification, and attachment state management.

Technical Details

Testing tools and configuration:
  • JUnit 4 test framework
  • Robolectric for Android runtime simulation
  • Mockito for mocking and verification
  • Custom test runners and SDK version management
  • Activity lifecycle simulation

Best Practices Demonstrated

The test suite demonstrates several testing best practices:
  • Thorough setup and teardown management
  • Systematic edge case coverage
  • Proper mock usage and verification
  • Clear test method naming and organization
  • Comprehensive assertion coverage

bumptech/glide

library/src/test/java/com/bumptech/glide/request/target/CustomViewTargetTest.java

            
package com.bumptech.glide.request.target;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.Display;
import android.view.View;
import android.view.View.OnAttachStateChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.transition.Transition;
import com.bumptech.glide.util.Preconditions;
import com.google.common.truth.Truth;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.android.controller.ActivityController;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;

/**
 * Test for {@link CustomViewTarget}.
 *
 * <p>TODO: This should really be in the tests subproject, but that causes errors because the R
 * class referenced in {@link CustomViewTarget} can't be found. This should be fixable with some
 * gradle changes, but I've so far failed to figure out the right set of commands.
 */
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 19, manifest = "build/intermediates/manifests/full/debug/AndroidManifest.xml")
public class CustomViewTargetTest {
  private ActivityController<Activity> activity;
  private View view;
  private ViewGroup parent;
  private CustomViewTarget<View, Object> target;
  @Mock private SizeReadyCallback cb;
  @Mock private Request request;
  private int sdkVersion;
  private AttachStateTarget attachStateTarget;

  @Before
  public void setUp() {
    sdkVersion = Build.VERSION.SDK_INT;
    MockitoAnnotations.initMocks(this);
    activity = Robolectric.buildActivity(Activity.class).create().start().postCreate(null).resume();
    view = new View(activity.get());
    target = new TestViewTarget(view);
    attachStateTarget = new AttachStateTarget(view);

    LinearLayout linearLayout = new LinearLayout(activity.get());
    View expandView = new View(activity.get());
    LinearLayout.LayoutParams linearLayoutParams =
        new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, /* height= */ 0);
    linearLayoutParams.weight = 1f;
    expandView.setLayoutParams(linearLayoutParams);
    linearLayout.addView(expandView);

    parent = new FrameLayout(activity.get());
    parent.addView(view);
    linearLayout.addView(parent);

    activity.get().setContentView(linearLayout);
  }

  @After
  public void tearDown() {
    setSdkVersionInt(sdkVersion);
    CustomViewTarget.SizeDeterminer.maxDisplayLength = null;
  }

  @Test
  public void testReturnsWrappedView() {
    assertEquals(view, target.getView());
  }

  @Test
  public void testReturnsNullFromGetRequestIfNoRequestSet() {
    assertNull(target.getRequest());
  }

  @Test
  public void testCanSetAndRetrieveRequest() {
    target.setRequest(request);

    assertEquals(request, target.getRequest());
  }

  @Test
  public void testRetrievesRequestFromPreviousTargetForView() {
    target.setRequest(request);

    CustomViewTarget<View, Object> second = new TestViewTarget(view);

    assertEquals(request, second.getRequest());
  }

  @Test
  public void testSizeCallbackIsCalledSynchronouslyIfViewSizeSet() {
    int dimens = 333;
    // activity.get().setContentView(view);
    view.layout(0, 0, dimens, dimens);

    target.getSize(cb);

    verify(cb).onSizeReady(eq(dimens), eq(dimens));
  }

  @Test
  public void testSizeCallbackIsCalledSynchronouslyIfLayoutParamsConcreteSizeSet() {
    int dimens = 444;
    LayoutParams layoutParams = new FrameLayout.LayoutParams(dimens, dimens);
    view.setLayoutParams(layoutParams);
    view.requestLayout();

    target.getSize(cb);

    verify(cb).onSizeReady(eq(dimens), eq(dimens));
  }

  @Test
  public void getSize_withBothWrapContent_usesDisplayDimens() {
    LayoutParams layoutParams =
        new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    view.setLayoutParams(layoutParams);

    setDisplayDimens(200, 300);

    activity.visible();
    view.layout(0, 0, 0, 0);

    target.getSize(cb);

    verify(cb).onSizeReady(300, 300);
  }

  @Test
  public void getSize_withWrapContentWidthAndValidHeight_usesDisplayDimenAndValidHeight() {
    int height = 100;
    LayoutParams params = new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, height);
    view.setLayoutParams(params);

    setDisplayDimens(100, 200);

    activity.visible();
    view.setRight(0);

    target.getSize(cb);

    verify(cb).onSizeReady(200, height);
  }

  @Test
  public void getSize_withWrapContentHeightAndValidWidth_returnsWidthAndDisplayDimen() {
    int width = 100;
    LayoutParams params = new FrameLayout.LayoutParams(width, LayoutParams.WRAP_CONTENT);
    view.setLayoutParams(params);
    setDisplayDimens(200, 100);
    parent.getLayoutParams().height = 200;

    activity.visible();

    target.getSize(cb);

    verify(cb).onSizeReady(width, 200);
  }

  @Test
  public void getSize_withWrapContentWidthAndMatchParentHeight_usesDisplayDimenWidthAndHeight() {
    LayoutParams params =
        new FrameLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    view.setLayoutParams(params);

    setDisplayDimens(500, 600);

    target.getSize(cb);

    verify(cb, never()).onSizeReady(anyInt(), anyInt());

    int height = 32;
    parent.getLayoutParams().height = height;
    activity.visible();

    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb).onSizeReady(500, height);
  }

  @Test
  public void getSize_withMatchParentWidthAndWrapContentHeight_usesWidthAndDisplayDimenHeight() {
    LayoutParams params =
        new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
    view.setLayoutParams(params);

    setDisplayDimens(300, 400);

    target.getSize(cb);

    verify(cb, never()).onSizeReady(anyInt(), anyInt());

    int width = 32;
    parent.getLayoutParams().width = 32;
    activity.visible();
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb).onSizeReady(width, 352);
  }

  @Test
  public void testMatchParentWidthAndHeight() {
    LayoutParams params =
        new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    view.setLayoutParams(params);

    target.getSize(cb);

    verify(cb, never()).onSizeReady(anyInt(), anyInt());

    activity.visible();
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb).onSizeReady(eq(parent.getWidth()), eq(parent.getHeight()));
  }

  @Test
  public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParams() {
    target.getSize(cb);

    int width = 12;
    int height = 32;
    parent.getLayoutParams().width = width;
    parent.getLayoutParams().height = height;
    activity.visible();
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb).onSizeReady(eq(width), eq(height));
  }

  @Test
  public void testSizeCallbacksAreCalledInOrderPreDraw() {
    SizeReadyCallback[] cbs = new SizeReadyCallback[25];
    for (int i = 0; i < cbs.length; i++) {
      cbs[i] = mock(SizeReadyCallback.class);
      target.getSize(cbs[i]);
    }

    int width = 100;
    int height = 111;
    parent.getLayoutParams().width = width;
    parent.getLayoutParams().height = height;
    activity.visible();
    view.getViewTreeObserver().dispatchOnPreDraw();

    InOrder order = inOrder((Object[]) cbs);
    for (SizeReadyCallback cb : cbs) {
      order.verify(cb).onSizeReady(eq(width), eq(height));
    }
  }

  @Test
  public void testDoesNotNotifyCallbackTwiceIfAddedTwice() {
    target.getSize(cb);
    target.getSize(cb);

    view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
    activity.visible();
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb, times(1)).onSizeReady(anyInt(), anyInt());
  }

  @Test
  public void testDoesNotAddMultipleListenersIfMultipleCallbacksAreAdded() {
    SizeReadyCallback cb1 = mock(SizeReadyCallback.class);
    SizeReadyCallback cb2 = mock(SizeReadyCallback.class);
    target.getSize(cb1);
    target.getSize(cb2);
    view.getViewTreeObserver().dispatchOnPreDraw();
    // assertThat(shadowObserver.getPreDrawListeners()).hasSize(1);
  }

  @Test
  public void testDoesAddSecondListenerIfFirstListenerIsRemovedBeforeSecondRequest() {
    SizeReadyCallback cb1 = mock(SizeReadyCallback.class);
    target.getSize(cb1);

    view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
    activity.visible();
    view.getViewTreeObserver().dispatchOnPreDraw();

    SizeReadyCallback cb2 = mock(SizeReadyCallback.class);
    view.setLayoutParams(
        new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
    target.getSize(cb2);

    view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb2).onSizeReady(anyInt(), anyInt());
  }

  @Test
  public void testSizeCallbackIsNotCalledPreDrawIfNoDimensSetOnPreDraw() {
    target.getSize(cb);
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb, never()).onSizeReady(anyInt(), anyInt());

    activity.visible();
    verify(cb).onSizeReady(anyInt(), anyInt());
  }

  @Test
  public void testSizeCallbackIsCalledPreDrawIfNoDimensAndNoLayoutParamsButLayoutParamsSetLater() {
    target.getSize(cb);

    int width = 689;
    int height = 354;
    LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
    view.setLayoutParams(layoutParams);
    view.requestLayout();
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb).onSizeReady(eq(width), eq(height));
  }

  @Test
  public void testCallbackIsNotCalledTwiceIfPreDrawFiresTwice() {
    activity.visible();
    target.getSize(cb);

    LayoutParams layoutParams = new FrameLayout.LayoutParams(1234, 4123);
    view.setLayoutParams(layoutParams);
    view.requestLayout();
    view.getViewTreeObserver().dispatchOnPreDraw();
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(cb, times(1)).onSizeReady(anyInt(), anyInt());
  }

  @Test
  public void testCallbacksFromMultipleRequestsAreNotifiedOnPreDraw() {
    SizeReadyCallback firstCb = mock(SizeReadyCallback.class);
    SizeReadyCallback secondCb = mock(SizeReadyCallback.class);
    target.getSize(firstCb);
    target.getSize(secondCb);

    int width = 68;
    int height = 875;
    LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
    view.setLayoutParams(layoutParams);
    activity.visible();
    view.getViewTreeObserver().dispatchOnPreDraw();
    view.getViewTreeObserver().dispatchOnPreDraw();

    verify(firstCb, times(1)).onSizeReady(eq(width), eq(height));
    verify(secondCb, times(1)).onSizeReady(eq(width), eq(height));
  }

  @Test
  public void testDoesNotThrowOnPreDrawIfViewTreeObserverIsDead() {
    target.getSize(cb);

    int width = 1;
    int height = 2;
    LayoutParams layoutParams = new FrameLayout.LayoutParams(width, height);
    view.setLayoutParams(layoutParams);
    ViewTreeObserver vto = view.getViewTreeObserver();
    view.requestLayout();
    activity.visible();
    assertFalse(vto.isAlive());
    vto.dispatchOnPreDraw();

    verify(cb).onSizeReady(eq(width), eq(height));
  }

  @Test(expected = NullPointerException.class)
  public void testThrowsIfGivenNullView() {
    new TestViewTarget(null);
  }

  @Test
  public void testDecreasesDimensionsByViewPadding() {
    activity.visible();
    view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
    view.setPadding(25, 25, 25, 25);
    view.requestLayout();

    target.getSize(cb);

    verify(cb).onSizeReady(50, 50);
  }

  @Test
  public void getSize_withValidWidthAndHeight_notLaidOut_notLayoutRequested_callsSizeReady() {
    view.setRight(100);
    view.setBottom(100);
    target.getSize(cb);

    verify(cb).onSizeReady(100, 100);
  }

  @Test
  public void getSize_withLayoutParams_notLaidOut_doesCallSizeReady() {
    view.setLayoutParams(new FrameLayout.LayoutParams(10, 10));
    view.setRight(100);
    view.setBottom(100);
    target.getSize(cb);

    verify(cb, times(1)).onSizeReady(anyInt(), anyInt());
  }

  @Test
  public void getSize_withLayoutParams_emptyParams_notLaidOutOrLayoutRequested_callsSizeReady() {
    view.setLayoutParams(new FrameLayout.LayoutParams(0, 0));
    view.setRight(100);
    view.setBottom(100);
    target.getSize(cb);

    verify(cb).onSizeReady(100, 100);
  }

  @Test
  public void getSize_withValidWidthAndHeight_preV19_layoutRequested_callsSizeReady() {
    setSdkVersionInt(18);
    view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
    view.requestLayout();

    target.getSize(cb);

    verify(cb).onSizeReady(100, 100);
  }

  @Test
  public void getSize_withWidthAndHeightEqualToPadding_doesNotCallSizeReady() {
    view.setLayoutParams(new FrameLayout.LayoutParams(100, 100));
    view.requestLayout();
    view.setPadding(50, 50, 50, 50);

    target.getSize(cb);

    verify(cb, never()).onSizeReady(anyInt(), anyInt());
  }

  private void setDisplayDimens(Integer width, Integer height) {
    WindowManager windowManager =
        (WindowManager)
            ApplicationProvider.getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
    Display display = Preconditions.checkNotNull(windowManager).getDefaultDisplay();
    if (width != null) {
      Shadows.shadowOf(display).setWidth(width);
    }

    if (height != null) {
      Shadows.shadowOf(display).setHeight(height);
    }
  }

  @Test
  public void clearOnDetach_onDetach_withNullRequest_doesNothing() {
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(null);
    activity.visible();
  }

  // This behavior isn't clearly correct, but it doesn't seem like there's any harm to clear an
  // already cleared request, so we might as well avoid the extra check/complexity in the code.
  @Test
  public void clearOnDetach_onDetach_withClearedRequest_clearsRequest() {
    activity.visible();
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(request);
    when(request.isCleared()).thenReturn(true);
    parent.removeView(view);

    verify(request).clear();
  }

  @Test
  public void clearOnDetach_onDetach_withRunningRequest_pausesRequestOnce() {
    activity.visible();
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(request);
    parent.removeView(view);

    verify(request).clear();
  }

  @Test
  public void clearOnDetach_onDetach_afterOnLoadCleared_removesListener() {
    activity.visible();
    attachStateTarget.clearOnDetach();
    attachStateTarget.onLoadCleared(/* placeholder= */ null);
    attachStateTarget.setRequest(request);
    parent.removeView(view);

    verify(request, never()).clear();
  }

  @Test
  public void clearOnDetach_moreThanOnce_registersObserverOnce() {
    activity.visible();
    attachStateTarget.setRequest(request);
    attachStateTarget.clearOnDetach().clearOnDetach();
    parent.removeView(view);

    verify(request).clear();
  }

  @Test
  public void clearOnDetach_onDetach_afterMultipleClearOnDetaches_removesListener() {
    activity.visible();
    attachStateTarget.clearOnDetach().clearOnDetach().clearOnDetach();
    attachStateTarget.onLoadCleared(/* placeholder= */ null);
    attachStateTarget.setRequest(request);
    parent.removeView(view);

    verify(request, never()).clear();
  }

  @Test
  public void clearOnDetach_onDetach_afterLoadCleared_clearsRequest() {
    activity.visible();
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(request);
    when(request.isCleared()).thenReturn(true);
    parent.removeView(view);

    verify(request).clear();
  }

  @Test
  public void clearOnDetach_onAttach_withNullRequest_doesNothing() {
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(null);
    activity.visible();
  }

  @Test
  public void clearOnDetach_onAttach_withRunningRequest_doesNotBeginRequest() {
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(request);
    when(request.isCleared()).thenReturn(false);
    activity.visible();

    verify(request, never()).begin();
  }

  @Test
  public void clearOnDetach_onAttach_withClearedRequest_beginsRequest() {
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(request);
    when(request.isCleared()).thenReturn(true);
    activity.visible();

    verify(request).begin();
  }

  @Test
  public void clearOnDetach_afterLoadClearedAndRestarted_onAttach_beginsRequest() {
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(request);
    when(request.isCleared()).thenReturn(true);
    attachStateTarget.onLoadCleared(/* placeholder= */ null);
    attachStateTarget.onLoadStarted(/* placeholder= */ null);
    activity.visible();

    verify(request).begin();
  }

  @Test
  public void clearOnDetach_onAttach_afterLoadCleared_doesNotBeingRequest() {
    attachStateTarget.clearOnDetach();
    attachStateTarget.setRequest(request);
    when(request.isCleared()).thenReturn(true);
    attachStateTarget.onLoadCleared(/* placeholder= */ null);
    activity.visible();

    verify(request, never()).begin();
  }

  @Test
  public void onLoadStarted_withoutClearOnDetach_doesNotAddListener() {
    activity.visible();
    target.setRequest(request);
    attachStateTarget.onLoadStarted(/* placeholder= */ null);
    parent.removeView(view);

    verify(request, never()).clear();
  }

  @Test
  public void onLoadCleared_withoutClearOnDetach_doesNotRemoveListeners() {
    final AtomicInteger count = new AtomicInteger();
    OnAttachStateChangeListener expected =
        new OnAttachStateChangeListener() {
          @Override
          public void onViewAttachedToWindow(View v) {
            count.incrementAndGet();
          }

          @Override
          public void onViewDetachedFromWindow(View v) {
            // Intentionally Empty.
          }
        };
    view.addOnAttachStateChangeListener(expected);

    attachStateTarget.onLoadCleared(/* placeholder= */ null);

    activity.visible();

    Truth.assertThat(count.get()).isEqualTo(1);
  }

  private static final class AttachStateTarget extends CustomViewTarget<View, Object> {
    AttachStateTarget(View view) {
      super(view);
    }

    @Override
    protected void onResourceCleared(@Nullable Drawable placeholder) {
      // Intentionally Empty.
    }

    @Override
    public void onLoadFailed(@Nullable Drawable errorDrawable) {
      // Intentionally Empty.
    }

    @Override
    public void onResourceReady(
        @NonNull Object resource, @Nullable Transition<? super Object> transition) {
      // Intentionally Empty.
    }
  }

  private static final class TestViewTarget extends CustomViewTarget<View, Object> {

    TestViewTarget(View view) {
      super(view);
    }

    @Override
    protected void onResourceCleared(@Nullable Drawable placeholder) {
      // Intentionally Empty.
    }

    // We're intentionally avoiding the super call.
    @SuppressWarnings("MissingSuperCall")
    @Override
    public void onResourceReady(
        @NonNull Object resource, @Nullable Transition<? super Object> transition) {
      // Avoid calling super.
    }

    // We're intentionally avoiding the super call.
    @SuppressWarnings("MissingSuperCall")
    @Override
    public void onResourceLoading(@Nullable Drawable placeholder) {
      // Avoid calling super.
    }

    // We're intentionally avoiding the super call.
    @SuppressWarnings("MissingSuperCall")
    @Override
    public void onLoadFailed(@Nullable Drawable errorDrawable) {
      // Avoid calling super.
    }
  }

  private static void setSdkVersionInt(int version) {
    ReflectionHelpers.setStaticField(Build.VERSION.class, "SDK_INT", version);
  }
}