Testing in React: Getting Off The Ground

A high level guide into testing your React Application

The following is intended as a lightweight introduction into testing in React.

UI testing

Testing the UI has always been a complex task. React made it easier to test the UI due to its render everything as soon as something changes approach. This means passing in the same data will also result in the same UI representation, making it easier to test components, as we can assert that the same input will always result into the same output. In this article we will solely focus on Unit Tests and neglect E2E testing approaches like using Selenium for example. Testing should incorporate both approaches. We will cover the first.


Tooling

All examples have been written with a basic setup consisting of Jasmine as assertion library and Karma as runner. This is not React specific and is it up to you to choose the proper tooling that fits your setup. There are multiple starter kits and write-ups covering numerous combinations including chai, expect and others. This part is up to you. Use your existing setup if you have one already. This will help to focus on the React specific testing aspects, further you can always switch runner, testing framework and reporting tools later on.

If you currently don’t have a JavaScript test setup, then either use the example config provided below or start by researching different options or choosing one of the many starter-kits/boilerplates out there. Possible alternatives to Karma/Jasmine is a Mocha/expect/jsdom combo for example. The choice is yours here.


First Things First: Setup and Configuration

The following examples will be based on a Webpack/Karma/Jasmine combination. Use the example package.json as a starting point or choose your own test up.

Example package.json


A first look: the Low Level Approach

Let’s start with a low level approach by using the React specific Test Utils. Test Utils enables a concept called shallow rendering, which simply means rendering a component one level deep, neglecting any child components.

import TestUtils from 'react-addons-test-utils';
const renderer = TestUtils.createRenderer();
function shallow(Component, props) {
renderer.render(<Component {...props} />);
return renderer.getRenderOutput();
}

By implementing our own shallow function, we can simply call shallowRenderer internally and return a shallow rendered component.

Now we can simply test a Bar component for example.

const Bar = React.createClass({
render() {
return (
<div className="bar">{this.props.title}</div>
)
}
});

Here’s our very first test, we want to make sure that the title passed in via props is being rendered.

describe('low level testing a React Application', () => {
it('should shallow render', () => {
const bar = shallow(Bar, {title: 'fooBar'});
expect(bar.type).toBe('div');
expect(bar.props.className).toBe('bar');
expect(bar.props.children).toBe('fooBar');
});
});

Shallow rendering doesn’t rely on any DOM, it returns a React Element one level deep meaning calling shallow(Bar, {title: ‘fooBar’}) will return an object.

{
$$typeof: ...,
type: 'div',
key: null,
ref: null,
props: {className: 'bar', children: 'bar'},
...
}

This also explains why we can assert bar.props.ClassName to be ‘fooBar’ for example.

Why shallow rendering? This approach has a couple of advantages but also a couple of drawbacks, obviously. One advantage is not having to deal with a real DOM, meaning no side effects. Further more, we can isolate the component quite comfortably, without having to depend on other components to function properly. The disadvantages include not being able to simulate clicks with Simulate (which we will get to later on) and no access to refs (references). There are workarounds to these limitations, but that also means a lot of extra work to implement in cases where we need to access references.


Second Step: Testing Component Composition

Shallow rendering is useful when trying to isolate a component, but if we want to fully render a component, including its child components, we will need a different mechanism. Let’s create a function called mount.

import { renderIntoDocument } from 'react-addons-test-utils';
function mount(Component, props) {
return renderIntoDocument(<Component { ...props } />);
}

All mount actually does at the moment is call React’s Test Utils renderIntoDocument function and return the result. It is important to note that renderIntoDocument requires a DOM. In contrast to shallowRenderer, which doesn’t rely on any DOM. We don’t need to update our setup, as previously mentioned we’re using karma with phantomJS, but also jsdom will work here, if you prefer mocha for example.

Let’s see how our previous Bar test might look like:

import React from 'react'
import
{ findDOMNode } from 'react-dom'
describe('the simplest way to test a react component', () => {
it ('mount should render a simple component', () => {
let component = mount(Bar, { title: 'fooBar' });
let node = findDOMNode(component);
expect(node.textContent).toEqual('fooBar')
});
});

Nothing special going on up until now. Only Interesting fact to note is that we’re using ReactDOM findDOMNode to retrieve the node element by passing in the React component.

Now what if we had a Baz component that would compose the Bar component?

const Baz = React.createClass({
render() {
return (
<div className="baz">
<Bar title={this.props.title} />
</div>
)
}
});

Let’s take a look at the test:

import { findRenderedDOMComponentWithClass } 
from 'react-addons-test-utils'
it('mount should also render children components', () => {
let component = mount(Baz, { title: 'foobarbaz' })
let node = findRenderedDOMComponentWithClass(component, 'bar')
expect(node.textContent).toEqual('foobarbaz')
})

We’re using findRenderedDOMComponentWithClass from ReactTestUtils, which returns the component in the rendered tree with the class name matching the specified name. Test Utils comes with a set of functions, like findRenderedDOMComponentWithTag and findRenderedComponentWithType for finding components by tag respectively type. Consult the docs for a full listing of available methods.


Step Three: Events

Obviously we will have test cases where we will need to trigger events. How do we go about simulating click events and typing inside an input? TestUtils comes with a method called Simulate, which has a method for every React supported event.

Let’s try to simulate a click on an item and check the number of completed tasks, which should be one, then again clicking on the same item and setting the completed tasks to 0 again. We’re still using our mount function for this example, this time passing in the Root component and then simulating a click on the first component with class name ‘todo’.

import { scryRenderedDOMComponentsWithClass as findByClass } 
from 'react-addons-test-utils';
it('should simulate click events', () => {
const app = mount(Root);
Simulate.click(findByClass(app, 'todo')[0]);
expect(findByClass(app, 'completed').length).toBe(1);
Simulate.click(findByClass(app, 'todo')[0]);
expect(findByClass(app, 'completed').length).toBe(0);
});

Improving mount

What if we could refactor the mount function to return some methods to quickly iterate over the rendered DOM? Well let’s try to implement a naive mount function that does exactly that.

function mount(Component) {
const find =
findByClass.bind(null, renderIntoDocument(Component));
return {
find(selector, nth) {
const selection = (typeof nth === 'undefined')
? find(selector)
: find(selector)[nth];
return {
simulate(eventType) {
Simulate[eventType](selection);
},
length: selection? selection.length : undefined
}
}
};
}

The new mount function returns an object with a find method which we could use to search for DOM elements inside the rendered root component. The new function will not get us too far either, but simplifies the previous example.

it('mount should simulate click events with a better interface', 
() => {
const app = mount(<Root />);
app.find('todo', 0).simulate('click');
expect(app.find('completed').length).toBe(1);
app.find('todo', 0).simulate('click');
expect(app.find('completed').length).toBe(0);
});

We should have an idea on how to approach testing from a low level perspective now. The good news is that we don’t need to write our own wrapper to handle shallow and full DOM rendering. Enzyme is a library that takes care of abstracting away the low level utils and offers three different high level approaches to testing a React application. Shallow, mount and render. The three approaches expose a similar Api offering convenience functions to make writing tests more efficient as well as removing boilerplate along the way.


Using a Testing Library

Enzyme makes writing tests a little easier by implementing a jQuery like API, enabling to chain methods and avoiding having to use the low level Test Utils. Actually enzyme is a wrapper around Test Utils. As previously mentioned, there are three different ways to test render your components: shallow, mount and render.

The previous shallow example would be written like this in Enzyme:

it('simple shallow rendering', function () {
expect(shallow(<Root />).find('.main').length).toBe(1)
})

Calling shallow returns a so called shallowWrapper object, and just like in our simple implementation it also exposes a find function for searching the object. While our find method only offered searching by class name, this implementation offers a number of ways to retrieve elements, including by tag, component, id and display name. find(‘#app’) or find(‘.someClass’) will all work. This simplifies searching for node elements.

it('simple render example', function () {
expect(render(<Root />).find('.todo').length).toBe(2);
});

render is very similar to shallow, but it relies on an external library (Cheerio) to parse the DOM as opposed to the shallowWrapper in the previous example. Another difference is that render also returns static HTML.

Mount finally mounts a React component into a real DOM node. The API is similar to shallow and render.

it('contains 2 todo items initially', function () {
expect(mount(<Root />).find('.todo').length).toBe(2);
});
it('contains some stuff like this or like that', function() {
const app = mount(<Root />);
const item = app.find('#item-1');
item.simulate('click');
expect(app.find('.completed').length).toBe(1);
item.simulate('click');
expect(app.find('.completed').length).toBe(0);
});

Deciding between that three approaches depends on what you’re trying to achieve. The shallow approach seems to be the recommended approach. Probably the best tactic is to start with shallow rendering and along the way see if there is a need for using mount. There will be cases and situations where mount will be the only useful way to write a test.


The wrap up: Final Example

We have seen a couple of approaches for testing in React. It might make sense to write a couple of tests, just to round things up.

Our example is heavily inspired by the thinking in React example. You can search your favorite J Dilla records and filter them based on the fact if they are out of print or not.

Here’s a link to the code.

Our app consists of a Root component that renders a SearchBar and a ReleaseTable component. The ReleaseTable itself renders a row for every found Release item. Let’s write a couple of tests for the Release component.

const Release = React.createClass({
render() {
const { title, artist, outOfPrint } = this.props.release;
const className = outOfPrint
? 'release outOfPrint' : 'release';
return (
<tr className={ className } >
<td className="artist">{ artist }</td>
<td className="title">{ title }</td>
<td className="comment">{ outOfPrint
? <span style={{color: 'red', fontStyle: 'italic'}}>
Out Of Print!
</span>
: null }
</td>
</tr>
);
}
});

The corresponding tests…

Let’s also write a couple of tests for the ReleaseTable component.

const ReleaseTable = React.createClass({
render() {
const { searchText, releases, noOutOfPrint } = this.props;
const rows = releases.filter((release) => {
return (release.title.indexOf(searchText) !== -1
|| release.artist.indexOf(searchText) !== -1)
&& !(release.outOfPrint && noOutOfPrint);
});

return (
<table className="releaseTable">
<thead>
<tr>
<th>Artist</th>
<th>Title</th>
<th>Comment</th>
</tr>
</thead>
<tbody>{rows.map(release => {
return <Release release={ release }
key={ release.title } />
})}</tbody>
</table>
);
}
});

The corresponding tests…

We will also write a couple of tests for SearchBar. This component is interesting as we need to simulate input updates and click events.

const SearchBar = React.createClass({
updateSearch() {
this.props.onSearch(
this.refs.search.value,
this.refs.noOutOfPrint.checked
);
},
render() {
return (
<form className="searchBar">
<input
type="text"
placeholder="Search..."
value=
{this.props.searchText}
ref="search"
onChange=
{this.updateSearch}
id="searchFilter"
/>
<p>
<input
type="checkbox"
checked=
{this.props.noOutOfPrint}
ref="noOutOfPrint"
onChange=
{this.updateSearch}
id="noOutOfPrint"
/>
Only show available releases
</p>
</form>
);
}
});

To test SearchBar we will have to use mount as opposed to all the previous tests, where we were able to use shallow. This is due to the fact that refs don’t exist when shallow rendering, but our component relies on refs to access the current input values. This means we can’t test if a change event will result in calling onSearch with the expected arguments. Mount enables us to test SearchBar properly. Our tests verify if changing the search text or clicking on the checkbox will update the filter settings. Also interesting to note is that we are using jasmine.createSpy to create a fake function and validate the result being passed when calling the fake onSearch.

There are more tests that could be written, but this should be enough to hit the ground and start running. I have added a number of useful resources at the end of this article. You can find the complete code for the example here.


This article is work in progress. If you have any questions or feedback please connect on twitter or leave a comment here.

Links

Approaches to testing React components — an overview

How React Components Make UI Testing Easy

React Test Utils

Enzyme Documentation

Jasmine

Jasmine Matchers

Expect

Like what you read? Give A. Sharif a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.