React Native: How to test Components implementing Animated with Jest

Jonathan Cardasis
Jan 22 · 2 min read

I’ve recently been creating components with more and more animation logic, primarily using React Native’s Animated library. When it comes to testing the component’s functionality, working around Animated timings and callbacks can be tricky.

We want to avoid async tests that wait for animations to complete to assert callback effects. In this brief article, we’ll look at how we can configure jest to allow us to validate Animated effects without any asynchronous actions.

Mocking Animated can work in some cases, but I’ve found implementing methods to time travel within a test to be faster and more deterministic.

Jest Configuration

First, if your package.json does not yet define a setupFiles array, create a new setup.js file and add a reference to your package.json.

...
"jest": {
...,
"setupFiles": [
"./jest/setup.js"
],
}

Next, install themockdate package for dev:

npm i mockdate --save-dev

In the setup.js file, declare the following:

function setupTimeTravelForRNAnimated() {
const MockDate = require('mockdate');
const frameTime = 10;
global.withAnimatedTimeTravelEnabled = (func) => {
MockDate.set(0);
jest.useFakeTimers();
func();
MockDate.reset();
jest.useRealTimers();
}
global.requestAnimationFrame = (callback) => {
setTimeout(callback, frameTime);
}
global.timeTravel = (time = frameTime) => {
const tickTravel = () => {
const now = Date.now();
MockDate.set(new Date(now + frameTime));
// Run the timers forward
jest.advanceTimersByTime(frameTime);
}
// Step through each of the frames
const frames = time / frameTime;
for (let i = 0; i < frames; i++) {
tickTravel();
}
}
}
setupTimeTravelForRNAnimated();

The default implementation ofrequestAnimationFrame normally calls setTimeout(callback, 0) which will result in a large number of timers being created. Jest watches the number of timers created and once it reaches a threshold, it assumes infinite recursion is taking place and can fail tests.

By settings the timeout to 10ms, we can avoid this issue.

The enableAnimatedTimeTravel andtimeTravel functions are convenience functions we will consume in our tests. We serially step through every 10ms of the animation, allowing RN Animated to process any callbacks needed.

Since this function is serial, our tests can time travel instantly, without the need for async tests 😎! Big shoutout this StackOverflow answer for the base functionality of this approach.

Example Jest Test

__tests__/MyComponent.js

import React from 'react';
import { shallow } from 'enzyme';
import MyComponent, {ANIMATION_TIME_OUT_SEC} from '../';
describe('MyComponent', () => {it('should close when special button is tapped', () => {
// ✨ Invoke our global time travel function
global.withAnimatedTimeTravelEnabled(() => {
const onDismiss = jest.fn();
const wrapper = shallow(<MyComponent visible={true} onDismiss={onDismiss} />);
const buttonComponent = wrapper.findWhere((node) => node.prop('testID') === 'my-special-button');

// Simulate press which triggers RN animation
buttonComponent.props().onPress();

// ✨ Simulate a time travel for RN Animated to trigger callbacks and effects
global.timeTravel(OPACITY_ANIMATION_OUT_TIME);
expect(onDismiss).toHaveBeenCalled();
});
});
});

Jonathan Cardasis

Written by

Senior Mobile Engineer at @Lyric with more than 7 years of experience in iOS/Android/React Native | jonathancardasis.com

More From Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade