React Testing Done Right

Testing is always a problem that we have to face sooner or later if we want our software to be more than a prototype.

React components have some peculiarities that make testing them different from ordinary classes. People tend to create complicated tests with numerous libraries while react and jest provide all the tools (renderers, snapshots, coverage, …) that we need to deal with this peculiarities and more.

In this article, I’ll present a series of concepts regarding testing best practices together with examples. At the same time, I’ve created a project containing all the code presented here and more: https://github.com/7ynk3r/react_test

Context

To start, let’s say that we want to render a shopping cart component using the following data

const cart = {
user: {
name: 'joe',
email: 'joe@mail.com',
},
items: [{
id: 1,
name: 'item 1',
price: 1,
},{
id: 2,
name: 'item 2',
price: 2,
}],
};

Let’s say as well that I’d like to include the total price for all the items and a way to delete an item from the list.

With this in mind, let’s deep into the sections.

Use Multiple Components

While we can solve the shopping cart problem in a single component’s render function, it’s a good idea to split the logic into smaller and simpler components that are easy to solve and test.

Looking at the payload for cart we can identify a few the components: Cart, User, Items, Item, Name, and Price. And looking at the additional requirements we can see we’ll need Total for total price and Delete to delete an item from the list.

Let’s see how Cart would look like

const Cart = ({ cart }) => (
<Container>
<User user={ cart.user } />
<Items items={ cart.items } />
</Container>
);
This is function component: has no state and is purely driven by the props it receives. If you want to learn more about function components, take a look at https://reactjs.org/docs/components-and-props.html#functional-and-class-components

This is simple and clean, and yet brings a question: can cart be null or undefined? what should we render in that case? what about cart.items? These are the kind of questions we have to ask ourselves while testing.

One Component One Test

Let’s take a look at another component, in this case, the more complex Items. This should render each Item in items and Total.

class Items extends React.Component {
constructor(props) {
super(props);
this.state = this.calculateState(props.items);
}
calculateState = (items) => ({
items,
total: items.map(item => item.value)
.reduce((a, b) => a + b, 0),
})
deleteItem = ({ id }) => {
const items = this.state.items.filter(item => item.id !== id);
this.setState(this.calculateState(items));
}
render = () => (
<Container>
{ this.state.items.map(item => (
<Item
key={ item.id }
item={ item }
onDelete={ () => this.deleteItem(item) }
/>
))}
<Total total={ this.state.total }/>
</Container>
)
}
In this case, I’ve used a class component because I need the state to deal with items being deleted.

This is a more complex component compared Cart, that has an internal state, that’s why it’s a good idea to test it be separate instead of doing it as part of Cart.

In the following sections, we’ll use Items to create a test that changes the internal component’s state.

Test Renderer

react comes with a package called react-test-renderer that allow us to “render” a component instead of using react-dom or react-native. What is the difference between them? Test renderer is faster than dom and native renderers but it isn’t rendering for real. That’s fine because we want to run our tests many times during development, that’s why fast is key.

Two renderer flavors

The package react-test-renderer provides two ways to render a component.

For shallow rendering, you can also use enzyme, which provides extra features like support for refs. If you want to learn more about it visit https://github.com/airbnb/enzyme

Snapshots

Snapshot comes as part of jest and with it, we can easily save the result of a test run and compare it with a future result, instead of having to write the result of the component by hand.

expect(tree).toMatchSnapshot();
For more information about snapshots, visit https://facebook.github.io/jest/docs/en/snapshot-testing.html

Comparison

To understand how snapshots and renderers work together let’s take User

const User = ({ user }) => (
<Title>{ user.name + ' - ' + user.email }</Title>
);
const Title = (props) => <Text { ...props } />

the following data

const user =  {
name: 'joe',
email: 'joe@mail.com',
};

and write some tests!

Using Deep Renderer

We can write a test like this

import renderer from 'react-test-renderer';
it('renders user deep correctly', () => {
const tree = renderer.create(<User user={ user }/>);
expect(tree).toMatchSnapshot();
});

After running it once, the auto-generated result of the snapshot will look like

exports[`renders user deep correctly 1`] = `
<Text
accessible={true}
allowFontScaling={true}
disabled={false}
ellipsizeMode="tail"
>
joe - joe@mail.com
</Text>
`;

We can observe that the renderer went all the way down in the hierarchy until it found Text.

Using Shallow Rendering

The test will look like

import ShallowRenderer from 'react-test-renderer/shallow';
it('renders user shallow correctly', () => {
const renderer = new ShallowRenderer();
renderer.render(<User user={ user }/>);
const tree = renderer.getRenderOutput();
expect(tree).toMatchSnapshot();
});

And the result of the snapshot

exports[`renders user shallow correctly 1`] = `
<Title>
joe - joe@mail.com
</Title>
`;

In this case the shallow renderer stopped resolving the components at the first level, instead of going to Text.

Breaking Tests

Imagine that we write the following test for Cart using the deep renderer.

it('renders cart correctly', () => {
const cart = {
user: {
name: 'joe',
email: 'joe@mail.com',
},
items: [{
id: 1, name: 'item 1', value: 7,
}, {
id: 2, name: 'item 2', value: 4,
}]
};
const tree = renderer.create(<Cart cart={ cart }/>);
expect(tree).toMatchSnapshot();
});

What will happen if we change the implementation of Title? Should the test for Cart break? I’d like not to but it will. Because the renderer will go all the way down in the hierarchy, it will render User and Title, which has changed.

Instead, if we use the shallow render, the result of the snapshot won’t change and our test for Cart won’t break. That’s why it’s better to use the shallow renderer.

State Changes

All the previous examples cover rendering a component given props. We’ll move over to a more interesting case, in which a change in the state will trigger a new render result.

Using the previously defined component Items, let’s create a test that renders two items and then deletes one of them.

it('renders one item deleted correctly', () => {
const renderer = new ShallowRenderer();
const items = [{
id: 1, name: 'item 1', value: 7,
}, {
id: 2, name: 'item 2', value: 4,
}];
  renderer.render(<Items items={ items }/>);
const result0 = renderer.getRenderOutput();
expect(result0).toMatchSnapshot();
  // access the first item and call `onDelete`.
result0.props.children[0][0].props.onDelete();
const result1 = renderer.getRenderOutput();
expect(result1).toMatchSnapshot();
});

The only “challenging” part of this test was finding onDelete because you have to navigate the children hierarchy.

You can see that we didn’t touch the state directly but through onDelete. This is considered a black box test because we didn’t access any internals directly during the test.

The test will generate the following two snapshots that reflect the desired behavior

exports[`renders one item deleted correctly 1`] = `
<Container>
<Item
item={
Object {
"id": 1,
"name": "item 1",
"value": 7,
}
}
onDelete={[Function]}
/>
<Item
item={
Object {
"id": 2,
"name": "item 2",
"value": 4,
}
}
onDelete={[Function]}
/>
<Total
total={11}
/>
</Container>
`;
exports[`renders one item deleted correctly 2`] = `
<Container>
<Item
item={
Object {
"id": 2,
"name": "item 2",
"value": 4,
}
}
onDelete={[Function]}
/>
<Total
total={4}
/>
</Container>
`;

Extras

High Order Components

HoC simplifies our lives by providing features that we can easily “plug” to any component, but make our tests more complex because they add an extra “layer”.

A simple solution is to export the components that we want to test separately as well as the default with the HoC.

export const Component = (props) => ...
export default highOrderComponent(...)(Component);
To learn more about HoC visit https://reactjs.org/docs/higher-order-components.html

Interfaces

A component will receive inputs props and will return outputs components. Those inputs and outputs conform the component’s interface.

Try to keep your interfaces simple and abstract whenever possible. This’ll make your code readable, maintainable and testable.

One clear indication of a poor interface can occur during testing. If writing tests for a component becomes hard, review your interfaces.

Conclusions

  • Break your components into multiple components and test each of them by separate.
  • Use the shallow renderer together with snapshots for simple and effective tests.
  • Avoid testing components and HoC at the same time, export and test each component by separate.
  • Spend the time to define good interfaces, they’ll make your life easier.

References