Straightforward React UI Testing
Using React.js props your application’s front-end. In particular, testing user interactions and viewing renderings is vastly improved with React UI testing.
Using React.js props your application’s front-end. In particular, testing user interactions and viewing renderings is vastly improved with React UI testing.
Swizec Teller is an author whose work has supported tens of thousands of engineers at Uber, Oracle, Apple and other prominent companies.
Editor’s note: This article was updated on 10/17/2022 by our editorial team. It has been modified to include recent sources and to align with our current editorial standards.
Fully testing user interactions is notoriously difficult. Front-end views weren’t well-suited for traditional automated testing frameworks until React.js came along.
With the advent of single page application (SPA) frameworks, our clients got heavier. Consequently, testing front-end code and UI components became more difficult to accomplish. Implementing test-driven development (TDD) into your project is a little weird at first, though it offers many perks: a predictable environment, multiple test runners, test tooling baked into the framework, and continuous integration support.
A decade ago, I would have said testing was the solution to all problems. But then Backbone got big and we all switched to front-end MVC, essentially turning our testable back ends into glorified database servers. With our most complicated code moved into the browser, our apps are no longer testable in practice, making front-end code and UI components difficult to test.
React natively includes models, functions, and components, all of which—by a sideways definition—may be considered to be units. React forces us to build components. All of this lays the groundwork for unit testing. In other words, React, by its very nature, lends itself to unit testing—a proven approach—within our UI/client.
To ensure our models are behaving well, or that calling a function changes the correct value, we execute the tests for the responsible units. To execute React UI testing, we need to:
- Write well-formed, isolated modules.
- Use Jasmine, Mocha, or other tests to run functions.
- Use a test runner, like Karma or Chutzpah.
And thus, our React code is unit tested.
It used to be that running front-end tests was the hard part. Frameworks were disparate. Also, in most cases, you would interface with a browser window that you manually refreshed in order to run tests. Of course, you could always forget—I know I did.
In 2012, Vojta Jina released the Karma runner (called Testacular at the time). With Karma, UI testing became a full citizen of the tool chain and our React tests run in a terminal or on a continuous integration server. Tests are automatically rerun when we modify a file, and we can test our code simultaneously in multiple browsers.
What more could we wish for? Well, to actually test our front-end React code.
UI Testing Requires More Than Just Unit Tests
Unit testing is perfect for fundamentals. It’s the best way to see if an algorithm performs consistently, or to check our input validation logic, or data transformations, or any other isolated operation.
But front-end code isn’t about manipulating data. It’s about user events and rendering the right views at the right time. Front ends are about the user experience.
Here’s what we would like to achieve in React testing:
- Test user events.
- Test the responses to user events.
- Ensure the right things render at the right time.
- Run tests in multiple browsers.
- Rerun tests on updated files.
- Work with continuous integration systems like Travis.
Before React, I hadn’t found a decent way to test user interaction and view rendering.
React Unit Testing: UI Components
Using React is the easiest way to guarantee we can achieve all of our testing goals. React forces us to architect apps using testable patterns that facilitate test writing. Testing is further supported by some fantastic React TestUtils
.
React’s components follow many of the best principles of functional programming, except they’re objects. For instance, given a single set of parameters, a React component will always render the same output—no matter how many times it’s rendered, no matter who renders it, no matter where we place the output. Consequently, we don’t have to perform complex scaffolding to test React components, or track global variables and config objects.
We achieve this steadiness by avoiding state as much as possible. In functional programming, we call this referrential transparency.
When it comes to testing user interactions, React has us covered with events bound to function callbacks. It’s easy to set up test spies and confirm that a click event calls the right function. And because React components render themselves, we can just trigger a click event and check the HTML for changes. This works because a React component is concerned only about itself, so a click here won’t change things there. We will never have to deal with a nest of event handlers—just well-defined function calls.
Because React is magic, we don’t have to worry about the Document Object Model (DOM). React uses the so-called virtual DOM to render components into a JavaScript variable. A reference to the virtual DOM is all we need in order to test React components.
React’s Built-in Test Utilities
React’s TestUtils
is our entry point to testing user interactions and checking the output. TestUtils
let us render a React component by putting its DOM in a variable (rather than inserting the DOM into a page). For example, we could render a React component by:
var component = TestUtils.renderIntoDocument(
<MyComponent />
);
Then, we could check whether all children were rendered:
var h1 = TestUtils.findRenderedDOMComponentWithTag(
component, 'h1'
);
We can now use getDOMNode()
to access the raw DOM element and test its values. As an example, let’s check whether our component’s H1
tag says “A title”:
expect(h1.getDOMNode().textContent)
.toEqual("A title");
Put together, the full test looks like:
it("renders an h1", function () {
var component = TestUtils.renderIntoDocument(
<MyComponent />
);
var h1 = TestUtils.findRenderedDOMComponentWithTag(
component, 'h1'
);
expect(h1.getDOMNode().textContent)
.toEqual("A title");
});
The findRenderedDOMComponentWithTag
does exactly what you might expect: It goes through the children, finds the component we’re looking for, and returns it. And the returned value behaves just like a React component.
TestUtils
lets us trigger user events too. For a click event, we could write:
var node = component
.findRenderedDOMComponentWithTag('button')
.getDOMNode();
TestUtils.Simulate.click(node);
The code simulates a click, and triggers any potential listeners, which should be component methods that change the output, the state, or both. The listeners can call a function on a parent component, if necessary.
All cases are simple to test: The changed state is in component.state
. We can access the output with normal DOM functions, and function calls with spies.
Why Not Jest?
React’s official documentation recommends Jest as a test runner and React UI testing framework. Built on top of Jasmine, Jest retains Jasmine’s syntax and benefits.
Editor’s note: Since this article was originally published, Jest has improved substantially. You can read our more recent tutorial, React Unit Testing Using Enzyme and Jest, and decide for yourself whether Jest is now up to the task.
Jest will mock everything except the component we are testing. This is fantastic, in theory, but I find it annoying. Anything we haven’t implemented yet, or that comes from a different part of the codebase, is just undefined
. While this may work for some cases, it could lead to quietly failing bugs. For example, in one case, I had had trouble testing a click event. No matter what I tried, it just would not call its listener. In time, I realized that Jest quietly “mocked away” the function without notifying me.
An even more critical deficiency in earlier versions of Jest was its lack of a watch mode. I prefer to run my tests in the background while I work. Watch mode enables me to automatically test changes. I don’t have to remember to run my test suite.
Jest doesn’t support running React tests in multiple browsers. This is less of an issue today than it was previously, but it’s still an important feature for that rare occasion where a heisenbug rears its head in, say, a specific version of Chrome.
React UI Testing: An Integrated Example
We’ve demonstrated how a good front-end React test should work, in theory. Let’s put our theory into action with a simple example.
Using a scatterplot component made with React and d3.js, let’s visualize different ways to generate random numbers. We’ll use Karma as our test runner, Mocha as a UI testing framework, and Webpack as a module loader. The code and demo are available at my React test site.
Setup
Our source files will go in the <root>/src
directory, and we’ll store tests in <root>/src/__tests__
. We can have several directories within src
, one for each major component, and each with its own test files. Bundling source code and test files in this way makes it easier to reuse React components for other projects.
With the directory structure in place, let’s install dependencies:
$ npm install --save-dev react d3 webpack babel-loader karma karma-cli karma-mocha karma-webpack expect
If anything fails to install, try rerunning that part of the installation. NPM failures are sometimes averted on a rerun.
Our package.json
file should look like this when we’re done:
{
"name": "react-testing-example",
"description": "A sample project to investigate testing options with ReactJS",
"scripts": {
"test": "karma start"
},
// ...
"homepage": "https://github.com/Swizec/react-testing-example",
"devDependencies": {
"babel-core": "^5.2.17",
"babel-loader": "^5.0.0",
"d3": "^3.5.5",
"expect": "^1.6.0",
"jsx-loader": "^0.13.2",
"karma": "^0.12.31",
"karma-chrome-launcher": "^0.1.10",
"karma-cli": "0.0.4",
"karma-mocha": "^0.1.10",
"karma-sourcemap-loader": "^0.3.4",
"karma-webpack": "^1.5.1",
"mocha": "^2.2.4",
"react": "^0.13.3",
"react-hot-loader": "^1.2.7",
"react-tools": "^0.13.3",
"webpack": "^1.9.4",
"webpack-dev-server": "^1.8.2"
}
}
After some configuration, you’ll be able to run tests with either npm test
or karma start
:
Configuration
To make sure Webpack knows how to find our code, and that Karma knows how to run the tests, add the following two lines of JavaScript to a ./tests.webpack.js
file:
var context = require.context('./src', true, /-test\.jsx?$/);
context.keys().forEach(context);
This code tells Webpack to consider anything with a -test
suffix to be part of the test suite.
Configuring Karma takes a bit more work:
// karma.conf.js
var webpack = require('webpack');
module.exports = function (config) {
config.set({
browsers: ['Chrome'],
singleRun: true,
frameworks: ['mocha'],
files: [
'tests.webpack.js'
],
preprocessors: {
'tests.webpack.js': ['webpack']
},
reporters: ['dots'],
webpack: {
module: {
loaders: [
{test: /\.jsx?$/, exclude: /node_modules/, loader: 'babel-loader'}
]
},
watch: true
},
webpackServer: {
noInfo: true
}
});
};
Most of these lines originate in a default Karma config. We used browsers
to indicate that tests should run in Chrome. frameworks
specifies the testing framework we’re using, and singleRun
is used to make tests run only once by default. You can keep Karma running in the background with karma start --no-single-run
. All simple enough.
Because Webpack handles our code’s dependency tree, we don’t have to specify all our files in the files
array. We only need tests.webpack.js
, which then requires all the necessary files.
We use the webpack
setting to tell Webpack what to do. In a normal environment, this part would go in a webpack.config.js
file.
We also tell Webpack to use the babel-loader
for our JavaScripts. This gives us all the fancy features from ECMAScript2015 and React’s JSX.
With the webpackServer
configuration, we tell Webpack not to print any debug info. It would only spoil our test output.
A React Component and a Test
With a running test suite, the rest is simple. We have to make a component that accepts an array of random coordinates and creates an <svg>
element with a bunch of points.
Following React UI testing best practices—i.e., standard TDD practice—we’ll write the test first, then the actual React component. Let’s start with a vanilla tests file in src/__tests__/
:
// ScatterPlot-test.jsx
var React = require('react/addons'),
TestUtils = React.addons.TestUtils,
expect = require('expect'),
ScatterPlot = require('../ScatterPlot.jsx');
var d3 = require('d3');
describe('ScatterPlot', function () {
var normal = d3.random.normal(1, 1),
mockData = d3.range(5).map(function () {
return {x: normal(), y: normal()};
});
});
First, we require React, its TestUtils
, d3.js, the expect
library, and the code we’re testing. Then we make a new test suite with describe
, and create some random data.
For our first test, let’s make sure ScatterPlot
renders a title. Our test goes inside the describe
block:
// ScatterPlot-test.jsx
it("renders an h1", function () {
var scatterplot = TestUtils.renderIntoDocument(
<ScatterPlot />
);
var h1 = TestUtils.findRenderedDOMComponentWithTag(
scatterplot, 'h1'
);
expect(h1.getDOMNode().textContent).toEqual("This is a random scatterplot");
});
Most tests will follow this pattern:
- Render.
- Find a specific node.
- Check contents.
As we demonstrated previously, renderIntoDocument
renders our component, findRenderedDOMComponentWithTag
finds the specific part we’re testing, and getDOMNode
gives us raw DOM access.
With no component to render a title tag, our test would fail. To make it pass, let’s write such a component:
var React = require('react/addons');
var d3 = require('d3');
var ScatterPlot = React.createClass({
render: function () {
return (
<div>
<h1>This is a random scatterplot</h1>
</div>
);
}
});
module.exports = ScatterPlot;
That’s it. The ScatterPlot
component renders a <div>
with an <H1>
tag containing the expected text, and our test will pass. Yes, it’s more than plain HTML, but bear with me a bit longer.
A More Interesting Test
I’d like to show you a test that ensures that all data points display on the chart:
// ScatterPlot-test.jsx
it("renders a circle for each datapoint", function () {
var scatterplot = TestUtils.renderIntoDocument(
<ScatterPlot data={mockData} />
);
var circles = TestUtils.scryRenderedDOMComponentsWithTag(
scatterplot, 'circle'
);
expect(circles.length).toEqual(5);
});
Same as before: Render, find nodes, check result. The interesting part here is drawing those DOM nodes. Let’s now add some d3.js magic to our component:
// ScatterPlot.jsx
componentWillMount: function () {
this.yScale = d3.scale.linear();
this.xScale = d3.scale.linear();
this.update_d3(this.props);
},
componentWillReceiveProps: function (newProps) {
this.update_d3(newProps);
},
update_d3: function (props) {
this.yScale
.domain([d3.min(props.data, function (d) { return d.y; }),
d3.max(props.data, function (d) { return d.y; })])
.range([props.point_r, Number(props.height-props.point_r)]);
this.xScale
.domain([d3.min(props.data, function (d) { return d.x; }),
d3.max(props.data, function (d) { return d.x; })])
.range([props.point_r, Number(props.width-props.point_r)]);
},
//...
You’ll find additional details in my React test repo.
We use componentWillMount
to set up empty d3 scales for the X
and Y
domains, and componentWillReceiveProps
to make sure they’re updated when something changes. Then update_d3
makes sure to set the domain
and the range
for both scales.
We’ll use the two scales to translate between random values in our dataset and positions on the picture. Most random generators return numbers in the [0,1
range, which is too small to see as pixels.
Then we add the points to our component’s render method:
// ScatterPlot.jsx
render: function () {
return (
<div>
<h1>This is a random scatterplot</h1>
<svg width={this.props.width} height={this.props.height}>
{this.props.data.map(function (pos, i) {
var key = "circle-"+i;
return (
<circle key={key}
cx={this.xScale(pos.x)}
cy={this.yScale(pos.y)}
r={this.props.point_r} />
);
}.bind(this))};
</svg>
</div>
);
}
This code goes through the this.props.data
array and adds a <circle>
element for each datapoint.
To learn more about combining React and d3.js to make data visualization components, consider subscribing to receive a free chapter from my book, React+d3.js.
Automated React Component Testing: Easier Than It Sounds
In this tutorial, you learned that:
- React forces us to modularize and encapsulate.
- Modular/encapsulated components facilitate the automation of React UI testing.
- Unit tests aren’t enough for front ends.
- Karma is a great test runner.
- Jest has come a long way toward being a great test runner.
I invite you again to check out my React test example repo, where you can review more React UI testing.
Further Reading on the Toptal Blog:
- Using RTK Query in React Apps With Redux Toolkit
- The Best React State Management Tools for Enterprise Applications
- React SEO Strategies and Best Practices
- React Test-driven Development: From User Stories to Production
- A Complete Guide to Testing React Hooks
- Working With React Hooks and TypeScript
- Tested Solutions: Working With React Design Patterns
Understanding the basics
What is component testing?
Component testing is similar to unit testing, but performed at a higher level, with the units integrated—at least partially. In some cases, component testing happens in the context of the overall application.
How do you test React components?
When designed using best practices, React components are modular and repeatable, making them testable in a manner that’s not unlike unit testing, without the rest of the application. Only a test framework (e.g., Jasmine) and a test runner (e.g., Karma) are needed to test React components.
What is meant by unit testing?
Unit testing is testing at the finest and most isolated levels possible, on a per-function basis. Unit testing reveals whether a function given a set of inputs returns the correct output.