Back to Repositories

Testing LottieValueAnimator Frame Control and Animation States in lottie-android

This test suite thoroughly validates the LottieValueAnimator component of the Lottie Android library, focusing on animation frame control, state management, and listener behavior. It ensures reliable animation playback and frame manipulation across various scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of LottieValueAnimator functionality, including:
  • Frame manipulation and boundary testing
  • Animation state management (play, pause, resume)
  • Direction control and speed reversal
  • Frame-to-fraction conversion accuracy
  • Min/max frame constraints
  • Animation listener lifecycle events

Implementation Analysis

The testing approach employs JUnit with Mockito for precise behavior verification. It uses a custom LottieValueAnimator implementation to handle frame callbacks, isolating the tests from Android’s Choreographer dependencies.

The suite implements systematic test patterns with mock listeners and atomic boolean flags to track animation completion states.

Technical Details

Key technical components include:
  • JUnit 4 test framework
  • Mockito for behavior verification
  • Custom animator implementation for frame control
  • AtomicBoolean for thread-safe state tracking
  • InOrder verification for sequence validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Thorough setup and teardown management
  • Isolation of Android dependencies
  • Comprehensive edge case coverage
  • Precise floating-point comparisons
  • Clear test method naming conventions
  • Systematic listener verification

airbnb/lottie-android

lottie/src/test/java/com/airbnb/lottie/LottieValueAnimatorUnitTest.java

            
package com.airbnb.lottie;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.graphics.Rect;
import androidx.collection.LongSparseArray;
import androidx.collection.SparseArrayCompat;
import com.airbnb.lottie.utils.LottieValueAnimator;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import org.mockito.Mockito;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicBoolean;

import static junit.framework.Assert.assertEquals;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.times;

public class LottieValueAnimatorUnitTest extends BaseTest {
  private interface VerifyListener {
    void verify(InOrder inOrder);
  }

  private LottieComposition composition;
  private LottieValueAnimator animator;
  private Animator.AnimatorListener spyListener;
  private InOrder inOrder;
  private AtomicBoolean isDone;

  @Before
  public void setup() {
    animator = createAnimator();
    composition = createComposition(0, 1000);

    animator.setComposition(composition);
    spyListener = Mockito.mock(Animator.AnimatorListener.class);
    isDone = new AtomicBoolean(false);
  }

  private LottieValueAnimator createAnimator() {
    // Choreographer#postFrameCallback hangs with robolectric.
    return new LottieValueAnimator() {
      @Override public void postFrameCallback() {
        running = true;
      }

      @Override public void removeFrameCallback() {
        running = false;
      }
    };
  }

  private LottieComposition createComposition(int startFrame, int endFrame) {
    LottieComposition composition = new LottieComposition();
    composition.init(new Rect(), startFrame, endFrame, 1000, new ArrayList<>(),
        new LongSparseArray<>(0), new HashMap<>(0),
        new HashMap<>(0), 1f, new SparseArrayCompat<>(0),
        new HashMap<>(0), new ArrayList<>(), 0, 0);
    return composition;
  }

  @Test
  public void testInitialState() {
    assertClose(0f, animator.getFrame());
  }

  @Test
  public void testResumingMaintainsValue() {
    animator.setFrame(500);
    animator.resumeAnimation();
    assertClose(500f, animator.getFrame());
  }

  @Test
  public void testFrameConvertsToAnimatedFraction() {
    animator.setFrame(500);
    animator.resumeAnimation();
    assertClose(0.5f, animator.getAnimatedFraction());
    assertClose(0.5f, animator.getAnimatedValueAbsolute());
  }

  @Test
    public void testPlayingResetsValue() {
    animator.setFrame(500);
    animator.playAnimation();
    assertClose(0f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
  }

  @Test
  public void testReversingMaintainsValue() {
    animator.setFrame(250);
    animator.reverseAnimationSpeed();
    assertClose(250f, animator.getFrame());
    assertClose(0.75f, animator.getAnimatedFraction());
    assertClose(0.25f, animator.getAnimatedValueAbsolute());
  }

  @Test
    public void testReversingWithMinValueMaintainsValue() {
    animator.setMinFrame(100);
    animator.setFrame(1000);
    animator.reverseAnimationSpeed();
    assertClose(1000f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
    assertClose(1f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testReversingWithMaxValueMaintainsValue() {
    animator.setMaxFrame(900);
    animator.reverseAnimationSpeed();
    assertClose(0f, animator.getFrame());
    assertClose(1f, animator.getAnimatedFraction());
    assertClose(0f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testResumeReversingWithMinValueMaintainsValue() {
    animator.setMaxFrame(900);
    animator.reverseAnimationSpeed();
    animator.resumeAnimation();
    assertClose(900f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
    assertClose(0.9f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testPlayReversingWithMinValueMaintainsValue() {
    animator.setMaxFrame(900);
    animator.reverseAnimationSpeed();
    animator.playAnimation();
    assertClose(900f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
    assertClose(0.9f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testMinAndMaxBothSet() {
    animator.setMinFrame(200);
    animator.setMaxFrame(800);
    animator.setFrame(400);
    assertClose(0.33333f, animator.getAnimatedFraction());
    assertClose(0.4f, animator.getAnimatedValueAbsolute());
    animator.reverseAnimationSpeed();
    assertClose(400f, animator.getFrame());
    assertClose(0.66666f, animator.getAnimatedFraction());
    assertClose(0.4f, animator.getAnimatedValueAbsolute());
    animator.resumeAnimation();
    assertClose(400f, animator.getFrame());
    assertClose(0.66666f, animator.getAnimatedFraction());
    assertClose(0.4f, animator.getAnimatedValueAbsolute());
    animator.playAnimation();
    assertClose(800f, animator.getFrame());
    assertClose(0f, animator.getAnimatedFraction());
    assertClose(0.8f, animator.getAnimatedValueAbsolute());
  }

  @Test
  public void testSetFrameIntegrity() {
    animator.setMinAndMaxFrames(200, 800);

    // setFrame < minFrame should clamp to minFrame
    animator.setFrame(100);
    assertEquals(200f, animator.getFrame());

    animator.setFrame(900);
    assertEquals(800f, animator.getFrame());
  }

  @Test(expected = IllegalArgumentException.class)
  public void testMinAndMaxFrameIntegrity() {
    animator.setMinAndMaxFrames(800, 200);
  }

  @Test
  public void testDefaultAnimator() {
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        inOrder.verify(spyListener, times(1)).onAnimationStart(animator, false);
        inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
        Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator);
      }
    });
  }

  @Test
  public void testReverseAnimator() {
    animator.reverseAnimationSpeed();
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        inOrder.verify(spyListener, times(1)).onAnimationStart(animator, true);
        inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, true);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
        Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator);
      }
    });
  }

  @Test
  public void testLoopingAnimatorOnce() {
    animator.setRepeatCount(1);
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false);
        Mockito.verify(spyListener, times(1)).onAnimationRepeat(animator);
        Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
      }
    });
  }

  @Test
  public void testLoopingAnimatorZeroTimes() {
    animator.setRepeatCount(0);
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationRepeat(animator);
        Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
      }
    });
  }

  @Test
  public void testLoopingAnimatorTwice() {
    animator.setRepeatCount(2);
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        Mockito.verify(spyListener, times(1)).onAnimationStart(animator, false);
        Mockito.verify(spyListener, times(2)).onAnimationRepeat(animator);
        Mockito.verify(spyListener, times(1)).onAnimationEnd(animator, false);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
      }
    });
  }

  @Test
  public void testLoopingAnimatorOnceReverse() {
    animator.setFrame(1000);
    animator.setRepeatCount(1);
    animator.reverseAnimationSpeed();
    testAnimator(new VerifyListener() {
      @Override public void verify(InOrder inOrder) {
        inOrder.verify(spyListener, times(1)).onAnimationStart(animator, true);
        inOrder.verify(spyListener, times(1)).onAnimationRepeat(animator);
        inOrder.verify(spyListener, times(1)).onAnimationEnd(animator, true);
        Mockito.verify(spyListener, times(0)).onAnimationCancel(animator);
      }
    });
  }

  @Test
  public void setMinFrameSmallerThanComposition() {
    animator.setMinFrame(-9000);
    assertClose(animator.getMinFrame(), composition.getStartFrame());
  }

  @Test
  public void setMaxFrameLargerThanComposition() {
    animator.setMaxFrame(9000);
    assertClose(animator.getMaxFrame(), composition.getEndFrame());
  }

  @Test
  public void setMinFrameBeforeComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setMinFrame(100);
    animator.setComposition(composition);
    assertClose(100.0f, animator.getMinFrame());
  }

  @Test
  public void setMaxFrameBeforeComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setMaxFrame(100);
    animator.setComposition(composition);
    assertClose(100.0f, animator.getMaxFrame());
  }

  @Test
  public void setMinAndMaxFrameBeforeComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setMinAndMaxFrames(100, 900);
    animator.setComposition(composition);
    assertClose(100.0f, animator.getMinFrame());
    assertClose(900.0f, animator.getMaxFrame());
  }

  @Test
  public void setMinFrameAfterComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    animator.setMinFrame(100);
    assertClose(100.0f, animator.getMinFrame());
  }

  @Test
  public void setMaxFrameAfterComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    animator.setMaxFrame(100);
    assertEquals(100.0f, animator.getMaxFrame());
  }

  @Test
  public void setMinAndMaxFrameAfterComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    animator.setMinAndMaxFrames(100, 900);
    assertClose(100.0f, animator.getMinFrame());
    assertClose(900.0f, animator.getMaxFrame());
  }

  @Test
  public void maxFrameOfNewShorterComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    LottieComposition composition2 = createComposition(0, 500);
    animator.setComposition(composition2);
    assertClose(500.0f, animator.getMaxFrame());
  }

  @Test
  public void maxFrameOfNewLongerComposition() {
    LottieValueAnimator animator = createAnimator();
    animator.setComposition(composition);
    LottieComposition composition2 = createComposition(0, 1500);
    animator.setComposition(composition2);
    assertClose(1500.0f, animator.getMaxFrame());
  }

  @Test
  public void clearComposition() {
    animator.clearComposition();
    assertClose(0.0f, animator.getMaxFrame());
    assertClose(0.0f, animator.getMinFrame());
  }

  @Test
  public void resetComposition() {
    animator.clearComposition();
    animator.setComposition(composition);
    assertClose(0.0f, animator.getMinFrame());
    assertClose(1000.0f, animator.getMaxFrame());
  }

  @Test
  public void resetAndSetMinBeforeComposition() {
    animator.clearComposition();
    animator.setMinFrame(100);
    animator.setComposition(composition);
    assertClose(100.0f, animator.getMinFrame());
    assertClose(1000.0f, animator.getMaxFrame());
  }

  @Test
  public void resetAndSetMinAterComposition() {
    animator.clearComposition();
    animator.setComposition(composition);
    animator.setMinFrame(100);
    assertClose(100.0f, animator.getMinFrame());
    assertClose(1000.0f, animator.getMaxFrame());
  }

  private void testAnimator(final VerifyListener verifyListener) {
    spyListener = Mockito.spy(new AnimatorListenerAdapter() {
      @Override public void onAnimationEnd(Animator animation) {
        verifyListener.verify(inOrder);
        isDone.set(true);
      }
    });
    inOrder = inOrder(spyListener);
    animator.addListener(spyListener);

    animator.playAnimation();
    while (!isDone.get()) {
      animator.doFrame(System.nanoTime());
    }
  }

  /**
   * Animations don't render on the out frame so if an animation is 1000 frames, the actual end will be 999.99. This causes
   * actual fractions to be something like .74999 when you might expect 75.
   */
  private static void assertClose(float expected, float actual) {
    assertEquals(expected, actual, expected * 0.01f);
  }
}