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 correctly
Invariant 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.