Unit testing React components without a DOM
- Update 12/04/17 Airbnb's Enzyme library offers the best solution for shallow rendering
- Update 04/01/16 Added better examples of skin-deep usage
When unit testing React components the common approach has been to render them into a DOM (with something like jsdom) and run some assertions against them with the help of the React TestUtils.
This has changed in 0.13 where an early implementation of shallow rendering is now ready to use.
Shallow rendering
Instead of rendering into a DOM the idea of shallow rendering is to instantiate
a component and get the result of its render
method, which is a
ReactElement.
From here you can do things like check its props and children and verify it
works as expected.
As you can imagine this is much faster (and less hassle) and will be the recommended way to test components in the future.
How it works
All you need to do is create an instance of the shallow renderer, render your component and then grab the output.
const React = require('react/addons');
const TestUtils = React.addons.TestUtils;
const shallowRenderer = TestUtils.createRenderer();
shallowRenderer.render(React.createElement(MyComponent, { className: 'MyComponent' }, 'some child text'));
const component = shallowRenderer.getRenderOutput();
This gives you an object that represents your component and looks roughly like the below (I've omitted some properties for the sake of brevity)
{
"type": "div",
"_store": {
"props": {
"className": "MyComponent",
"children": [{
"type": "h2",
"_store": {
"props": {
"className": "MyComponent-header",
"children": "Title"
},
"originalProps": {
"className": "MyComponent-header",
"children": "Title"
}
}
}]
}
}
}
And now you can test against it
expect(component.props.className).to.equal('MyComponent');
Reusing the shallowRenderer
with skin-deep
Once you start using shallow rendering there will be a need to create rendered versions of components in each test, so it makes sense to move the logic into a reusable module.
skin-deep is an excellent library to help with this.
I'd recommend that instead you try Enzyme.
import React from 'react';
import sd from 'skin-deep';
const tree = sd.shallowRender(React.createElement(MyComponent, {}));
const instance = tree.getMountedInstance();
const vdom = tree.getRenderOutput();
The tree
object gives you access to various useful methods (check the
documentation
for more info) which allow you to dig down into the tree of components and run
assertions against things like text and props.
Example: Testing a Post
component
Now that the basics have been explained let's work through a simple example of
testing a fictional Post
component. It will accept a title and content and
ensure any paragraph tags on the content are stripped away:
import React from 'react';
const {div, h2, p} = React.DOM;
export default React.createClass({
displayName: 'Post',
propTypes: {
title: React.PropTypes.string.isRequired,
content: React.PropTypes.string
},
stripParagraphTags(html) {
return html.replace(/<\/?p>/g, '');
},
doSomethingOnClick(event) {
this.setState({isClicked: true});
event.preventDefault();
},
render() {
const content = this.stripParagraphTags(this.props.content);
return (
div({className: 'Post'},
h2({className: 'Post-header', onClick: this.doSomethingOnClick}, this.props.title),
p({className: 'Post-content'}, content)
)
);
}
});
The Post
component spec
I've chosen to use Chai and Mocha for my unit tests, but you're free to use Jest, Jasmine or any other test runner and assertion library.
First we'll set up some boilerplate before we start writing actual tests
import { expect } from 'chai';
import React from 'react';
import Post from '../../components/post.react';
import sd from 'skin-deep';
describe('Post component', function() {
let tree;
beforeEach(() => {
tree = sd.shallowRender(React.createElement(Post, {title: 'Title', content: '<p>Content</p>'}));
});
});
This is all that is needed. Wonderfully simple.
Now we can make use of skin-deep to query the render tree and ensure parts of our component have the text rendered as expected.
it('should render a post title and content', () => {
expect(tree.subTree('.Post-header').text()).to.equal('Title');
expect(tree.subTree('.Post-content').text()).to.equal('Content');
});
Testing component methods
It's not uncommon to have a few methods attached to the React component and to need to test them. An example might be a method that performs some complex transforms on data sent in via the props.
If you stick to pure functions on your React components then it's much easier to unit test them.
You can reference any method directly on the prototype of the component. Let's
make sure the stripParagraphTags
method is working correctly.
describe('stripParagraphTags method', () => {
it('should strip <p> tags', () => {
const strippedText = Post.prototype.stripParagraphTags('<p>Some text.</p> <p>More text.</p>');
expect(strippedText).to.equal('Some text. More text.');
});
});
If you cannot avoid a pure function and depend on things like this.props
then it is possible to access the actual component instance instead.
const instance = tree.getMountedInstance();
instance.stripParagraphTags('<p>Content</p>');
This will be correctly bound with the React component.
It might be advisable to treat this as an anti-pattern though as it's not as
robust as just testing a pure function. One way to avoid it is to pass the props
required into the function, rather than relying on this
:
this.someComponentMethod(this.props.text);
A good use for getMountedInstance
is to allow access to the component state,
which we will see an example of next.
Testing event handlers
Event handlers can be tested in a similar way, but you will need to provide your
own mocked event object if things like preventDefault
are used. Let's test our
Post
click handler.
describe('doSomethingOnClick method', () => {
it('should modify the `isClicked` state property', () => {
const header = tree.subTree('.Post-header');
header.props.onClick({
preventDefault() {}
});
expect(tree.getMountedInstance().state).to.eql({isClicked: true});
});
});
Here we are passing a very simple event
mock that just has an empty function,
but this could also be a good candidate for Sinon JS if
more complex assertion was needed.
Running the tests
To verify it all works we can run Mocha with Babel to take care of the ES6 compilation.
Post component
✓ should render a post title and content
doSomethingOnClick method
✓ should modify the `isClicked` state property
stripParagraphTags method
✓ should strip <p> tags
3 passing (183ms)
Great, our first test is passing.
Rendering a list of Posts
Now that the Post
component is rendering and passing the unit tests we recieve
a requirement to render a list of them from a set of data and ensure that it
works as expected.
To do this we'll reach for another React component called PostList
and it will
just be responsible for taking a set of data and rendering a Post
for each
item of data.
Keeping components separated like this is can allow reuse in different contexts and ensure components do one job well. Composition is encouraged and is one of Reacts' strengths.
The PostList
component
Nothing too fancy here, just creating a new Post
component for each item in
the posts
data set that is passed in as a prop.
import React from 'react';
import Post from './post.react';
const {ul, li} = React.DOM;
export default React.createClass({
displayName: 'PostList',
renderListItems(posts) {
return (
posts.map(post =>
li({key: post.id, className: 'PostList-item'},
React.createElement(Post, {title: post.title, content: post.content})
)
)
);
},
render() {
return ul({className: 'PostList'}, this.renderListItems(this.props.posts));
}
});
In terms of what to test here it seems sensible to just make sure the PostList
has rendered a Post
for each data item. With the above code we could expect an
HTML output like this:
<ul class="PostList">
<li class="PostList-item">
<div class="Post"><!-- content --></div>
</li>
<!-- And repeat... -->
</ul>
With that in mind we will write a spec to ensure each <li>
contains a Post
component and that the total matches the total set of posts in the data source.
import { expect } from 'chai';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
import PostList from '../../components/PostList.react';
import Post from '../../components/post.react';
import sd from 'skin-deep';
describe('PostList component', () => {
const postData = [
{ id: 1, title: 'Title 1', content: '<p>Content 1</p>' },
{ id: 2, title: 'Title 2', content: '<p>Content 2</p>' },
{ id: 3, title: 'Title 3', content: '<p>Content 3</p>' }
];
it('should render a list of post components', () => {
const tree = sd.shallowRender(React.createElement(PostList, {posts: postData}));
const items = tree.everySubTree('Post');
expect(items.length).to.equal(postData.length);
});
});
Using everySubTree
returns an array of all the Post
components. Now the
length of the items
array should match the total items in the postData
array.
PostList component
✓ should render a list of post components
Post component
✓ should render a post title and content
doSomethingOnClick method
✓ should modify the `isClicked` state property
stripParagraphTags method
✓ should strip <p> tags
4 passing (183ms)
And it does! Perfect.
Testing the actual rendering
Areas of testing that this doesn't cover would be interactions with components (form fields, buttons etc) and visual rendering. I find this sort of acceptance testing is best left to tools like Browserstack or Sauce Labs as they can test multiple devices and operating systems and will paint a more accurate picture of how your application behaves.
I've found unit testing with shallow rendering is best used to ensure the application data is passing through your components as intended but you can make them as granular as you like.
Conclusion and example code
We've now written and unit tested two separate React components without even needing a DOM or web browser. Although it's still an experimental feature I would recommend trying it out and seeing how it fits in with your code base.
You can grab all the example code in this repository on GitHub and find more information on the React documentation page.