React Native: How to test Components implementing Animated with Jest
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();
});
});
});