Test Animations - A Travel through Time

Cover Photo

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.

Time Passage

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

Sand Through Hands

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();
  });
});

References