Testing

Note

All the examples in this guide are using jest as the testing framework & react-testing-library. If you wish to see these examples using other testing frameworks or perhaps there are caveats that are specific to another testing framework, please open an issue or a PR.

Introduction

In this guide, we'll go through testing a basic component that uses useSpring and animated from the library:

const FadeIn = ({ isVisible, children }) => {
const styles = useSpring({
opacity: isVisible ? 1 : 0,
y: isVisible ? 0 : 24,
})
return <animated.div style={styles}>{children}</animated.div>
}

Our component takes two props, children & more importantly, isVisible. When isVisible is true, we want to fade in the component and when it's false, we want to fade it out. We could test the y position of the element, but for the purposes of this guide, we'll focus on the opacity.

So, let's start by writing a test for this component.

Example

import { render, screen } from '@testing-library/react'
import { animated, useSpring } from '@react-spring/web'
import { FadeIn } from './FadeIn'
test('Correctly renders the FadeIn component', async () => {
const { rerender } = render(<FadeIn>Hello!</FadeIn>)
const element = screen.getByText('Hello!')
expect(element).toHaveStyle('opacity: 0')
})

This initial test is pretty simple, we're just rendering the component and asserting that the opacity is 0 when the component is initially mounted, and by all means it will be pass. Now we want to look at testing that the opacity of element changes when the isVisible prop changes. So we modify our test to look like this:

import { render, screen } from '@testing-library/react'
import { animated, useSpring } from '@react-spring/web'
import { FadeIn } from './FadeIn'
test('Correctly renders the FadeIn component', () => {
const { rerender } = render(<FadeIn>Hello!</FadeIn>)
const element = screen.getByText('Hello!')
expect(element).toHaveStyle('opacity: 0')
rerender(<FadeIn isVisible>Hello!</FadeIn>)
expect(element).toHaveStyle('opacity: 1')
})

This test will fail with an error that will look something like this:

expect(element).toHaveStyle()
- Expected
- opacity: 1;
+ opacity: 0;
21 | rerender(<FadeIn isVisible>Hello!</FadeIn>);
> 23 | expect(element).toHaveStyle("opacity: 1");
24 | });

And if you're familiar with how jest works you won't be surprised by this error. The problem is that useSpring animates values, that is the value isn't set immediately, but rather it's changed over time. So, we could wait for the animation to become what we expect by using waitFor:

import { render, screen, waitFor } from '@testing-library/react'
import { animated, useSpring } from '@react-spring/web'
import { FadeIn } from './FadeIn'
test('Correctly renders the FadeIn component', async () => {
const { rerender } = render(<FadeIn>Hello!</FadeIn>)
const element = screen.getByText('Hello!')
expect(element).toHaveStyle('opacity: 0')
rerender(<FadeIn isVisible>Hello!</FadeIn>)
await waitFor(() => {
expect(element).toHaveStyle('opacity: 1')
})
})

And this would pass, but now we're waiting for the animation to change and update and this adds more time to your tests, time that's a bit unnecessary because we're not interested in the visual effects in this scenario, we want to know the updates are correctly made.

Skipping Animations

The solution to this problem is to skip animations when testing. This can be done by using the Globals object and calling the assign method setting skipAnimations to true. You can do this immediately in the setup file for your tests or if you want more granual control then you could use the beforeAll | beforeEach hooks to set it.

import { render, screen, waitFor } from '@testing-library/react'
import { animated, useSpring, Globals } from '@react-spring/web'
import { FadeIn } from './FadeIn'
beforeAll(() => {
Globals.assign({
skipAnimation: true,
})
})
test('Correctly renders the FadeIn component', async () => {
const { rerender } = render(<FadeIn>Hello!</FadeIn>)
const element = screen.getByText('Hello!')
expect(element).toHaveStyle('opacity: 0')
rerender(<FadeIn isVisible>Hello!</FadeIn>)
await waitFor(() => {
expect(element).toHaveStyle('opacity: 1')
})
})

This would then set the opacity immediate to 1 and the test would pass. However, we still are required to use waitFor because the update requires a tick of the environment to reflect the changes.

Fake Timers

Alternatively, if you want to keep your code simpler by avoiding an async call for waitFor you could opt to use jest.useFakeTimers and manually advance the environment:

import { render, screen, waitFor } from '@testing-library/react'
import { animated, useSpring, Globals } from '@react-spring/web'
import { FadeIn } from './FadeIn'
beforeAll(() => {
Globals.assign({
skipAnimation: true,
})
jest.useFakeTimers()
})
test('Correctly renders the FadeIn component', async () => {
const { rerender } = render(<FadeIn>Hello!</FadeIn>)
const element = screen.getByText('Hello!')
expect(element).toHaveStyle('opacity: 0')
rerender(<FadeIn isVisible>Hello!</FadeIn>)
jest.advanceTimersByTime(1)
expect(element).toHaveStyle('opacity: 1')
})

Troubleshooting

ESM modules not handled by jest

path/to/project/node_modules/@react-spring/web/react-spring-web.esm.js.js:1
({"Object.<anonymous>":function(module,exports,require,dirname,filename,global,jest){import _objectWithoutPropertiesLoose from '@babel/runtime/helpers/esm/objectWithoutPropertiesLoose';

You may have come across this message when testing with jest. If you have, this is because jest is incorrectly resolving the correct file type for the library. It in fact wants to be using the cjs file type. To fix this, you can add the following to your jest.config.js file:

module.exports = {
moduleNameMapper: {
'@react-spring/web':
'<rootDir>/node_modules/@react-spring/web/react-spring-web.cjs.js',
},
}

This could be applicable to any target you're using e.g. @react-spring/native.