Test Animations - A Travel through Time
Photo by Dino Reichmuth on Unsplash
TL;DR: check the Implementation section.
Taking advantage of Testing approaches to help writing better software has already been proven. Sometimes, it is necessary some effort to set up mocks to test specific details of the code or specific behaviors of the application. For React Native, that’s the case with Animations.
Increment small portions of the App with some Animations has been made easy with the Animated API provided by the React Native Framework out of the box. But trying to test those small transitions and elements appearing, hiding, or even changing at the screen seems not to be that easy. And even on researching about it, there are almost no resources that could be found or propose a good abstract solution.
Thoughts
Animations happen through time. So, why just not wait until it happens after the trigger inside the test and test the result?
Well, the usage of test tools that are not specific for End-to-End testing generally doesn’t have any approach to manage the passage of time. Async calls are commonly mocked, preventing them from happening and removing the asynchronous behavior of the test. This is good and desired as unit and integration tests tend to test just small portions of the Application.
On the other hand, the usage of some End-to-End tools could lead to so much more overhead. The focus is on so much more than just one simple animation that do some small change in one screen of the Application.
Proposal
Mock the Passage of Time.
Photo by Lukas Blazek on Unsplash
Abstraction
For implementing the solution it is necessary
- Use fake timers from
jest
- Mock the passage of time
When running Animations it is necessary to tell jest
to use “fake” timers so it handles that properly. And for Mock the passage of time, it is possible to do that with some libraries. The most common are:
jest-date-mock
MockDate
As the article written by Benjamin Jonson uses MockDate
, here it’ll be covered the usage of jest-date-mock
.
Implementation
First, it is necessary to tell jest
about using fake timers. This could be done at a simple setup.js
file, if this is not already made at the project
// jest/setup.js
jest.useFakeTimers('legacy');
After that, it is necessary to install jest-date-mock
if it is not installed yet
yarn add jest-date-mock -D
and tell to the jest.config.js
file or inside package.json
jest
configs that this setup file must be called before the suites of tests start
// jest.config.js
module.exports = {
// ...
setupFilesAfterEnv: [
// ...
'./jest/setup.js',
'jest-date-mock', // <- this is necessary by jest-date-mock config
],
};
And finally, create the Time Travel module
// jest/time-travel.js
import { advanceBy, advanceTo, clear } from 'jest-date-mock';
const FRAME_TIME = 10;
function advanceOneFrame() {
advanceBy(FRAME_TIME);
jest.advanceTimersByTime(FRAME_TIME);
}
/**
* Setup tests for time travel (start date)
*/
export function setup(startDate = '') {
advanceTo(new Date(startDate));
}
/**
* Travel a specific amount of time (in ms) inside a test
*/
export function travel(time = FRAME_TIME) {
let framesToRun = time / FRAME_TIME;
while (framesToRun > 0) {
advanceOneFrame();
framesToRun -= 1;
}
}
/**
* End test with time travel
*/
export function teardown() {
clear();
}
Usage
Photo by Ben White on Unsplash
Using the Time Travel module is simple:
- Call
setup()
at the start of your test - Trigger one Animation
- Call
travel()
passing some time - Test the result
- Repeat until no more Animations remain
- Call
teardown()
at the end of the test
If there are more than one test inside a suite of tests that makes use of Animations, it is also possible to use the beforeEach()
/beforeAll()
and afterEach()
/afterAll()
functions.
The final code should be something like
import 'react-native';
import React from 'react';
import { render, fireEvent } from '@testing-library/react-native';
import Component from './Component';
import { setup, travel, teardown } from '../jest/time-travel';
describe('Component', () => {
it('test with animation', () => {
// prepare to test
setup();
// render your Component
const { getByText } = render(<Component />);
// trigger the animation
fireEvent.press(getByText('Pressable Element'));
// move through time
travel(500);
// expect results
// ...
// finish testing
teardown();
});
});