De-mystifying Jest Snapshot Test Mocks
So, let’s say you have a nice React Native setup with the Jest testing library. You want to snapshot-test all your components of course! But you’re getting seemingly unrelated errors when you tried to mock a third party module in your snapshots and you’re lost in all that API documentation. Let’s dig into an example and get a clear picture of what’s happening under the hood.
A Simple Example
No story starts without an example. We initialised a small project with React Native, added Flow and wrote a Jest snapshot test that utilises react-test-renderer
to render the main component output.
We run jest
. It will eventually give you this output:
PASS __tests__/App.js
App
✓ renders correctly (16ms)Snapshot Summary
› 1 snapshot written in 1 test suite.Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 added, 1 total
Time: 1.148s, estimated 10s
Ran all test suites.
It passes, so everything is fine. A snapshot of the component tree was recorded and put in a separate .js.snap
file.
Now, let’s add a third party module: the well known react-native-vector-icons
module. This is simply done from the command line as follows:
yarn add react-native-vector-icons
react-native link react-native-vector-icons
We render an icon in the app by simply importing an Icon
component and adding some JSX code in the functional App
component:
import Icon from 'react-native-vector-icons/FontAwesome';// ...export default function App() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome fellow traveler!
</Text>
<Icon name="thumbs-up" size={64} color="#9ad3f1" />
</View>
);
}
The full code looks like this. When we run jest
again, we get a snapshot error with the following diff:
@@ -20,6 +20,27 @@
}
}
>
Welcome fellow traveler!
</Text>
+ <Text
+ accessible={true}
+ allowFontScaling={false}
+ ellipsizeMode="tail"
+ style={
+ Array [
+ Object {
+ "color": "#9ad3f1",
+ "fontSize": 64,
+ },
+ undefined,
+ Object {
+ "fontFamily": "FontAwesome",
+ "fontStyle": "normal",
+ "fontWeight": "normal",
+ },
+ ]
+ }
+ >
+
+ </Text>
</View>
What we’re seeing here is the render output of the Icon
component. We luckily did not get exceptions, because the Icon
component does not render custom native views (but that
does look a bit fishy).
We could store this result in our snapshot, but I advise you not to do that. If the module owner ever changed his mind about how a vector icon should be drawn on screen, our test would start to fail when we upgrade the module dependency.
What we want to do is mock the implementation of Icon
and just return a React element that represents the parameters with which the Icon
was created. There’s a small piece in the docs about it. So let’s try it out. We add the following mock setup to our test:
jest.mock('react-native-vector-icons/FontAwesome', () => 'Icon');
The first parameter specifies the exact module import you want to mock, and the second parameter is a factory function that returns the actual mock. More about that below.
We rerun jest
, still expecting it to fail, but now with a prettier component tree:
@@ -20,6 +20,11 @@
}
}
>
Welcome fellow traveler!
</Text>
+ <Icon
+ color="#9ad3f1"
+ name="thumbs-up"
+ size={64}
+ />
</View>
Now, we actually see the representation of our native module instead of its internal rendering. Great! This is something we want to store in our snapshot.
We run jest -u
and get a test pass:
PASS __tests__/App.js
App
✓ renders correctly (78ms)Snapshot Summary
› 1 snapshot updated in 1 test suite.Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 updated, 1 total
Time: 1.094s, estimated 2s
Ran all test suites.
You can find the commit here.
jest.mock Under The Hood
So, what’s happening under the hood with that jest.mock
call? If we would just call:
jest.mock('react-native-vector-icons/FontAwesome');
… this would replace the output of that specific import with a generated (or ‘auto’) mock implementation. The mock implementation is created with the same shape as the entire module export, but with mocked behaviour: all ‘mirrored’ functions or components render null
(when not configured otherwise — more about that later).
But rendering null
in a snapshot scenario means it won’t show up in our snapshot tree. That’s why we want to provide a different mocking behaviour by providing a factory function.
The most simple factory function returns a string, like () => 'SomeName'
. React actually accepts a string as a ‘component’, which is a shorthand for rendering a named element with its provided props and children.
So, when we write:
jest.mock('react-native-vector-icons/FontAwesome', () => 'Icon');
… then the default export of the react-native-vector-icons/FontAwesome
module will be a single component that renders an element named Icon
whenever it is rendered by the app.
Custom Mocking
But there are more components provided in the vector icons module: for instance, there’s the Icon.Button
component.
When we add that component to our App render function:
import Icon from 'react-native-vector-icons/FontAwesome';// ...function App() {
return (
<View>
<Icon name="thumbs-up" size={64} color="#9ad3f1" />
<Icon.Button name="facebook" backgroundColor="#3b5998">
Login with Facebook
</Icon.Button>
</View>
);
}
…the output of jest
gets a lot less understandable:
FAIL __tests__/App.js
● App › renders correctlyInvariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in. Check the render method of `App`.at invariant (node_modules/fbjs/lib/invariant.js:44:15)
at instantiateReactComponent (node_modules/react-test-renderer/lib/instantiateReactComponent.js:77:56)
...
React gives you a few hints of what’s going on. It got an element type that is undefined
instead of a string or a class or function. React hints that you might have tried to use a component that was not properly exported.
Remember that we only provided a shorthand string component for our custom mock? The actual Icon
component function contains a property Icon.Button
which is not mocked at all—it is undefined
after mocking and that causes the error.
We can change the mock factory function to provide a more elaborate mock component:
jest.mock('react-native-vector-icons/FontAwesome', () => {
const React = require('react');
const Icon = props => React.createElement('Icon', props, props.children);
Icon.Button = 'Icon.Button';
return Icon;
});// ... tests
Instead of returning a string, we now want to mirror the Icon
component with its property Button
. We create a simple component that mimics the behaviour of providing a shorthand string: it creates a named React element with provided properties and children. And in JavaScript, a function can have properties, so we assign the property Button
on the function to be a shorthand string component.
Now, when we run jest
we no longer get an error, but just an expected snapshot failure, as seen in this commit:
@@ -25,6 +25,12 @@
<Icon
color="#9ad3f1"
name="thumbs-up"
size={64}
/>
+ <Icon.Button
+ backgroundColor="#3b5998"
+ name="facebook"
+ >
+ Login with Facebook
+ </Icon.Button>
</View>
It is a good idea to create a helper function in a separate file for creating mocking components that always can be extended:
/*
@flow
@providesModule mockComponent
*/
import React from 'react';export default function mockComponent(name: string): Function {
return (props: Object) => React.createElement(name, props, props.children);
}
By specifying @providesModule
we can import
or require
the helper function easily in our mock setup, without relative file paths. Now you can create a mirrored structure for your mocks like this:
jest.mock('react-native-vector-icons/FontAwesome', () => {
const mockComponent = require('mockComponent').default;
const Icon = mockComponent('Icon');
Icon.Button = mockComponent('Icon.Button');
return Icon;
});// ... tests
See the full code in this commit.
You might wonder: why not use an import
statement outside this function instead of that require
? That’s because jest.mock
calls are hoisted before all import
statements by the babel-jest-plugin
. This leads to mockComponent
not being defined when this function is interpreted and later evaluated. By using require
inside this function we can work around that issue.
Mocking ES6 Modules
If you look at the module exports of react-native-vector-icons/FontAwesome.js
you’ll see that not only Icon
is exported (with Icon.Button
and others) but that Button
is also exported as an ES6 module named export. So you can import a Button component with the following line as well:
import { Button } from 'react-native-vector-icons/FontAwesome';
(Actually, this is never mentioned in the react-native-vector-icons
documentation as an official API but probably there for legacy reasons).
Now we get in some trouble again, as jest.mock
only replaces the default export with a mock implementation and seems incapable of letting you override named exports. But actually, jest.mock
can be instructed to mock an entire module export!
We use jest.genMockFromModule
to create an ES6 module export structure, as seen in this commit:
jest.mock('react-native-vector-icons/FontAwesome', () => {
const mockComponent = require('mockComponent').default;
const module = jest.genMockFromModule('react-native-vector-icons/FontAwesome');
const Icon = mockComponent('Icon');
Icon.Button = mockComponent('Icon.Button');
module.default = Icon;
module.Button = Icon.Button;
return module;
});
The trick here is that jest.genMockFromModule
creates an object with the property __esModule
set to true
. This flag is picked up by jest.mock
and switches its behaviour to mocking ES6 module exports. We could have returned an object with this flag set, but it seems to be undocumented and quite private, so I advise to use a public API to achieve this.
Manual mock
Finally, if we have multiple tests that render Icon
components, it would be cumbersome to calljest.mock
with this implementation in each test. Of course we can export it in a separate file and import it in each test, or include it in our test setup file, but there’s another Jest mechanism to make this a bit easier. We will set up a manual mock.
For this we need to create a __mocks__
folder next to the node_modules
folder. Then, we need to recreate folders for each folder in the import name: in this case only react-native-vector-icons
. In that folder, place a JavaScript file called FontAwesome.js
.
This file will now automatically become the mock implementation in tests whenever react-native-vector-icons/FontAwesome
is imported. And the beauty is that you can simply use ES6 import
and export
statements in the manual mock file, so no need for jest.genMockFromModule
tricks.
So the contents of __mocks__/react-native-vector-icons/FontAwesome.js
should look something like this, as shown in this commit:
/* @flow */
import mockComponent from 'mockComponent';const Icon = mockComponent('Icon');
Icon.Button = mockComponent('Icon.Button');export default Icon;export const Button = Icon.Button;
We can now remove the jest.mock
call from our test and see that we still have passing tests — it is picked up automatic (so far for the ‘manual’ part).
But this can easily lead to misunderstandings in larger code bases, so it is a good idea to show future developers that the component is actually being mocked. This can be simply done by adding a call to jest.mock
without a factory function:
jest.mock('react-native-vector-icons/FontAwesome');// tests
Yes, a manual mock overrides the default behaviour of jest.mock
when not providing a factory function, so this is perfectly valid.
Now, if React Native component authors would provide Jest manual mocks of their components to relieve developers of writing those mocks over and over again…
Concluding
I hope you now have a clear understanding of what’s happening when a module is mocked with Jest and that you’re able to mock a native dependency properly. In a followup post I’ll cover mocking several other native components.
All the code in this post can be found at this GitHub repo.