Shallow rendering revisited: Better unit tests for React/Preact components
Feb 10, 2020 · 8-minute read
In this post I will discuss the benefits of isolating tests for React/Preact UI components from the details of child components, and the various approaches that we looked at for our frontend projects at Hypothesis. I will explain why we settled on the approach that we did and provide links if you would like to use the same tools and techniques in your projects.
Why mock child components?
When writing unit tests for a module in an application, it is generally a good idea to isolate that module from its dependencies, or at least those which have significant complexity or cost in terms of runtime performance. Isolating such dependencies has several benefits:
-
The setup code for the module you are testing becomes simpler because it doesn’t have to set up everything that the module’s dependencies need in order to function
-
The tests will run faster because expensive logic, I/O or platform API calls in dependencies are replaced with a faster alternative, such as returning a hard-coded result
-
The tests are more likely to be reliable. One of the biggest pain points in large test suites is often flakey tests. Any source of unreliability in a module is magnified by the number of tests that execute that module’s code.
-
Test failures are easier to debug, because there is less code “in scope” which might need to be looked at to understand the cause of the failure.
-
The process of writing tests for a module provides feedback about the API of that module. Isolating a module from its dependencies helps to surface design problems with the interaction between those modules.
All of these benefits compound as the application grows. They may only provide modest benefits initially, but can make a huge difference in a large codebase which has evolved over several years at the hands of many developers.
The above comments refer to code generally, and they apply just as well to
UI components. For a UI component, one of the main kinds of dependency they may
have is child components which they render.
For example an ActivityFeed
might render several Story
items which may in
turn contain many other components for timestamps, photos, comment boxes
etc.
Approaches to mocking child components in React/Preact apps
The classic approach: Shallow rendering
In React, the originally recommended approach to isolating the tests for a UI component from the details of its child components was to use React’s “shallow renderer”.
The shallow renderer renders a component “one level deep”. This means that child components remain un-expanded in the rendered output instead of being rendered themselves. Tests can inspect the output to see which child components were rendered and what props were passed to them.
We initially used shallow rendering via the Enzyme
testing library’s shallow
function. Although it provides
the isolation we were looking for, we found that it has several significant
disadvantages:
-
It doesn’t render actual DOM nodes. This means that you cannot test details of a component that depend on this. There are other differences from “real” rendering as well.
-
What the shallow renderer treats as a “unit” doesn’t match what is logically a “unit” of your application. If you take a component and split its internals into multiple components within the same module to avoid duplication or improve readability, that shouldn’t require changing your tests, assuming the end result is the same. However since shallow rendering only renders one level of the component tree, without understanding which components come from the same vs. different modules, it does.
This issue also affects components that are wrapped, eg. to connect the component to a Redux store. Introducing or removing a wrapper will require changes to the tests.
-
Facebook don’t actively use the shallow renderer any more and as a result, it is not being actively developed, although given the volume of existing code that relies on it in the wild, I don’t expect it to disappear any time soon. See this GitHub issue and the React testing guide for more details.
-
It only works for React UI components. You need to use another method to mock other types of dependency that your component has (eg. functions that make network requests). This increases the overall amount of infrastructure that new developers to your team need to learn about in order to be productive.
The zero-tooling approach: Test seams
A simple way to mock child components that does not require any extra tooling is to pass the child components as props, with default values set to the normal implementation. It might look something like this:
// SomeWidget.js
import ComplexChildWidget from './ComplexChildWidget';
export default function SomeWidget({ aProp, anotherProb, ComplexChildWidget = ComplexChildWidget }) {
return (
<div>
...
<ComplexChildWidget …/>
</div>
);
}
// SomeWidget-test.js
describe('SomeWidget', () => {
it('renders expected output', () => {
const container = document.createElement('div');
function DummyChildWidget({ … }) {
return <div>Dummy widget</div>;
}
render(<SomeWidget aProp="test" ComplexChildWidget={DummyChildWidget} …/>, container);
// Check that `DummyChildWidget` was used as expected.
});
});
This is an example of providing a test seam. The downside of this
approach is that it requires manually adding extra props to tests for each
component we want to be able to replace in tests. The extra code also clutters the
implementation of SomeWidget
.
The React Testing Guide’s suggested approach: Jest mocking
React’s Testing Guide recommends an alternative approach using the Jest test runner‘s built-in mocking facilities. It provides an example of mocking a component that loads Google Maps.
Using Jest mocking resolves several of the issues with the shallow renderer mentioned above:
-
Tests render real DOM nodes, so tests can dispatch events on DOM nodes and test other details that depend on DOM behavior.
-
Only components imported from other modules are mocked. If a component is refactored into sub-components in the same file, or wrapped in some way, tests will not be affected.
A downside to this approach is that it requires you to use the Jest test runner. There are various reasons why you might not want or be able to use Jest. For example it currently uses a “fake” DOM implementation (JSDOM) in Node and doesn’t yet have a supported solution for running in an actual browser.
Our application depends heavily on browser APIs which were historically not well supported by JSDOM, so we use Karma and mocha. More recently, we have also added accessibility tests that make use of the ability to query the outputs of rendering components with their real CSS.
Our approach: Mocking with a Babel plugin
An alternative way to mock dependencies that is not tied to any particular test runner or JavaScript environment is to use a compiler plugin (for Babel or TypeScript) to transform the code in the test environment.
We use a Babel plugin, babel-plugin-mockable-imports, for this purpose.
This plugin adds an $imports
object to each module which it processes. This
object can be imported in tests and its $mock
and $restore
methods can be
used to temporarily mock any import and later undo those mocks. Mocking an
imported React (or Preact) component might look like this:
import Widget, { $imports } from '../Widget';
describe('Widget', () => {
beforeEach(() => {
const ImportedComponent = (props) => props.children;
$imports.$mock({
'./ImportedComponent': ImportedComponent,
});
});
afterEach(() => {
$imports.$restore();
});
});
A particularly useful feature of this plugin is that it supports semi-automatic
mocking, where $mock
is passed a function which is called with each import
in the module, and it either generates an appropriate mock, or skips mocking.
This can be used to systematically mock all the UI components that a module imports in a consistent way:
$imports.$mock((source, symbol, value) => {
if (typeof value === 'function' && 'propTypes' in value) {
// This looks like a React component, generate a mock for it.
const mockComponent = props => props.children;
mockComponent.displayName = value.displayName || value.name;
return mockComponent;
} else {
return null; // Skip mocking
}
});
To make UI tests easier to write and ensure consistency, we extract this mocking function into a utility which is used in the setup of each UI test.
Given an implementation of a UI component (SomeWidget.js
):
import { createElement } from 'preact';
import ComplexChildWidget from './ComplexChildWidget';
function SomeHelper({ … }) {
return (
<ul>
…
</ul>
);
}
export default function SomeWidget({ … }) {
return (
<div>
...
<SomeHelper …/>
<ComplexChildWidget someProp={aValue} …/>
</div>
);
}
The tests might look something like this (SomeWidget-test.js
):
import { mount } from 'enzyme';
import { mockImportedComponents } from '../test-utils';
import SomeWidget, { $imports } from '../SomeWidget.js';
describe('SomeWidget', () => {
beforeEach(() => {
$imports.$mock(mockImportedComponents);
});
afterEach(() => {
$imports.$restore();
});
// Tests for `SomeWidget` go here.
it('renders a ComplexChildWidget with the right props', () => {
const widget = mount(<SomeWidget …/>);
const child = widget.find('ComplexChildWidget');
assert.equal(child.prop('someProp'), 'expectedValue');
});
});
Compared to Jest mocking, there are some caveats. The main one is that code at the top level of a module is not re-evaluated when a mock is applied. We’ve rarely found this to be a significant limitation in practice.
Using these tools in your project
The Babel plugin has a README that explains how to install and use it in your project. There are examples for JavaScript and TypeScript.
The code for the mockImportedComponents
utility can be found
here.
Feel free to copy and adapt as necessary. In the same repository you can find
real examples
of UI component tests in our application.
Feedback
If you have comments or questions, please file an issue on the Babel plugin’s repository or contact me via email or Twitter.