How do I unit test Android code that has View Animators?
Asked Answered
D

3

5

I have an instance where a couple buttons are shown and hidden depending on which page in a ViewPager is being shown. The are shown and hidden with Animators. Is there a way to check for/delay unit testing until this has been completed?

I'm using Robolectric since that's probably relevant. I tried calling Robolectric.runUiThreadTasksIncludingDelayedTasks(); but this didn't seem to fix anything.

The animation code is as follows:

public static void regularFadeView(final boolean show, final View view) {
    view.animate()
            .setInterpolator(mDecelerateInterpolator)
            .alpha(show ? 1 : 0)
            .setListener(new SimpleAnimatorListener() {
                @Override
                public void onAnimationStart(Animator animation) {
                    if (show) view.setVisibility(View.VISIBLE);
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    if (!show) view.setVisibility(View.INVISIBLE);
                }
            })
            .start();
}
Discriminant answered 5/5, 2014 at 20:27 Comment(0)
D
3

I ended up creating an AnimationUtility interface and a real and fake implementations. The fake implementation immediately set the view to visible/hidden instead of doing the animation. I dynamically inject the real/fake one depending on the proper context.

Discriminant answered 6/5, 2014 at 22:24 Comment(0)
A
6

I think you could solve this problem rearranging the approach. This is, by extracting the SimpleAnimatorListener to a protected variable, and then unit test based on that. Something like:

@VisibleForTesting
SimpleAnimatorListener getAnimationListener(boolean show, View view) {
   return new SimpleAnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {
            if (show) view.setVisibility(View.VISIBLE);
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            if (!show) view.setVisibility(View.INVISIBLE);
        }
    }

public static void regularFadeView(boolean show, View view) {
     view.animate()
        .setInterpolator(mDecelerateInterpolator)
        .alpha(show ? 1 : 0)
        .setListener(getAnimationListener(show, view))
        .start();
}

And then on your test:

private void shouldShowViewWhenShowIsTrue() {
     View mockedView = Mockito.mock(View.class);
     SimpleAnimatorListener animationListener = getAnimationListener(true, mockedView);
     animationListener.onAnimationStart(null);
     Mockito.verify(mockedView).setVisibility(View.VISIBLE);
}

Even better could be to have instead of a method like getAnimationListener(), would be to create a FadeAnimationListener that would extend SimpleAnimatorListener, and put the animation logic there.

Hope this helps!

Acetaldehyde answered 5/5, 2014 at 21:42 Comment(2)
Thanks, this got me going in the right direction, though this is mainly a test for the listener itself, no?Discriminant
Humm I don't know man. My approch with testing is thinking the class under test like a black box. I don't care about the internals. My tests are give/when/then so in this case the then is the view is visible after the animation, not internal class to show it with a fancy animation has been called. Another solution is Robolectric.getUiThreadScheduler().advanceBy(4000) ; I can't get it to work, but the idea is betterNicotiana
D
3

I ended up creating an AnimationUtility interface and a real and fake implementations. The fake implementation immediately set the view to visible/hidden instead of doing the animation. I dynamically inject the real/fake one depending on the proper context.

Discriminant answered 6/5, 2014 at 22:24 Comment(0)
P
0

Here I put my solution based on ValueAnimator classes. I use mockk library.

fun mockObjectAnimators() {
    mockkStatic(ObjectAnimator::class)
    val targetSlot = slot<Any>()
    val propertySlot = slot<String>()
    every {
        ObjectAnimator.ofFloat(capture(targetSlot), capture(propertySlot), *anyFloatVararg())
    } answers {
        spyk(
            ObjectAnimator().apply {
                target = targetSlot.captured
                duration = 0L
                setPropertyName(propertySlot.captured)
            }
        ).also { spy ->
            every { spy.start() } answers {
                spy.listeners.forEach { it.onAnimationStart(spy) }
                spy.listeners.forEach { it.onAnimationEnd(spy) }
            }
            every { spy.setDuration(any()) } answers { spy }
        }
    }
}

For ViewPropertyAnimator you could try similar approach

Poleax answered 30/11, 2020 at 15:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.