Testing
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 does 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 infact 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
.