This repo shows you how to test React component. It is loosely based on Jack Franklin's article "Testing React Applications".
Demo
$ git clone https://github.com/ruanyf/react-testing-demo.git
$ cd react-testing-demo && npm install
$ npm start
$ open http://127.0.0.1:8080
Now, you visit http://127.0.0.1:8080/, and should see a Todo app.
There are 5 places to test.
- App's title should be "Todos"
- Initial state of a Todo item should be right ("done" or "undone")
- Click a Todo item, its state should be toggled (from "undone" to "done", or vice versa)
- Click a Delete button, the Todo item should be deleted
- Click the Add Todo button, a new Todo item should be added into the TodoList
All test cases have been written. You run npm test
to find the test result.
$ npm test
Index
Testing Library
The most important tool of testing React is official Test Utilities, but it only provides low-level API. As a result, some third-party test libraries are built based on it. Airbnb's Enzyme library is the easiest one to use among them.
Thus every test case has at least two ways to write.
- Test Utilities' way
- Enzyme's way
This repo will show you both of them.
React official Test Utilities
Since a component could be rendered into either a virtual DOM object (React.Component
's instance) or a real DOM node, Test Utilities library gives you two testing choices.
- Shallow Rendering: testing a virtual DOM object
- DOM Rendering: testing a real DOM node
Shallow Rendering
Shallow Rendering just renders a component "one level deep" without worrying about the behavior of child components, and returns a virtual DOM object. It does not require a DOM, since the component will not be mounted into DOM.
At first, import the Test Utilities in your test case script.
import TestUtils from 'react-addons-test-utils';
Then, write a Shallow Rendering function.
import TestUtils from 'react-addons-test-utils';
function shallowRender(Component) {
const renderer = TestUtils.createRenderer();
renderer.render(<Component/>);
return renderer.getRenderOutput();
}
In the code above, we define a function shallowRender
to return a component's shallow rendering.
The first test case is to test the title of App
. It needn't interact with DOM and doesn't involve child-components, so is most suitable for use with shadow rendering.
describe('Shallow Rendering', function () {
it('App\'s title should be Todos', function () {
const app = shallowRender(App);
expect(app.props.children[0].type).to.equal('h1');
expect(app.props.children[0].props.children).to.equal('Todos');
});
});
You may feel app.props.children[0].props.children
intimidating, but it is not. Each virtual DOM object has a props.children
property which contains its all children components. app.props.children[0]
is the h1
element whose props.children
is the text of h1
.
The second test case is to test the initial state of a TodoItem
is undone.
At first, we should modify the function shallowRender
to accept second parameter.
import TestUtils from 'react-addons-test-utils';
function shallowRender(Component, props) {
const renderer = TestUtils.createRenderer();
renderer.render(<Component {...props}/>);
return renderer.getRenderOutput();
}
The following is the test case.
import TodoItem from '../app/components/TodoItem';
describe('Shallow Rendering', function () {
it('Todo item should not have todo-done class', function () {
const todoItemData = { id: 0, name: 'Todo one', done: false };
const todoItem = shallowRender(TodoItem, {todo: todoItemData});
expect(todoItem.props.children[0].props.className.indexOf('todo-done')).to.equal(-1);
});
});
In the code above, since TodoItem
is a child component of App
, we have to call shallowRender
function with TodoItem
, otherwise it will not be rendered. In our demo, if the state of a TodoItem
is undone, the class
property (props.className
) contains no todo-done
.
renderIntoDocument
The second testing choice of official Test Utilities is to render a React component into a real DOM node. renderIntoDocument
method is used for this purpose.
import TestUtils from 'react-addons-test-utils';
import App from '../app/components/App';
const app = TestUtils.renderIntoDocument(<App/>);
renderIntoDocument
method requires a DOM, otherwise throws an error. Before running the test case, DOM environment (includes window
, document
and navigator
Object) should be available. So we use jsdom to implement the DOM environment.
import jsdom from 'jsdom';
if (typeof document === 'undefined') {
global.document = jsdom.jsdom('<!doctype html><html><body></body></html>');
global.window = document.defaultView;
global.navigator = global.window.navigator;
}
We save the code above into test/setup.js
. Then modify package.json
.
{
"scripts": {
"test": "mocha --compilers js:babel-core/register --require ./test/setup.js",
},
}
Now every time we run npm test
, setup.js
will be required into test script to run together.
The third test case is to test the delete button.
describe('DOM Rendering', function () {
it('Click the delete button, the Todo item should be deleted', function () {
const app = TestUtils.renderIntoDocument(<App/>);
let todoItems = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li');
let todoLength = todoItems.length;
let deleteButton = todoItems[0].querySelector('button');
TestUtils.Simulate.click(deleteButton);
let todoItemsAfterClick = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li');
expect(todoItemsAfterClick.length).to.equal(todoLength - 1);
});
});
In the code above, first, scryRenderedDOMComponentsWithTag
method finds all li
elements of the app
component. Next, get out todoItems[0]
and find the delete button from it. Then use TestUtils.Simulate.click
to simulate the click action upon it. Last, expect the new number of all li
elements to be less one than the old number.
Test Utilities provides many methods to find DOM elements from a React component.
- scryRenderedDOMComponentsWithClass: Finds all instances of components in the rendered tree that are DOM components with the class name matching className.
- findRenderedDOMComponentWithClass: Like scryRenderedDOMComponentsWithClass() but expects there to be one result, and returns that one result, or throws exception if there is any other number of matches besides one.
- scryRenderedDOMComponentsWithTag: Finds all instances of components in the rendered tree that are DOM components with the tag name matching tagName.
- findRenderedDOMComponentWithTag: Like scryRenderedDOMComponentsWithTag() but expects there to be one result, and returns that one result, or throws exception if there is any other number of matches besides one.
- scryRenderedComponentsWithType: Finds all instances of components with type equal to componentClass.
- findRenderedComponentWithType: Same as scryRenderedComponentsWithType() but expects there to be one result and returns that one result, or throws exception if there is any other number of matches besides one.
- findAllInRenderedTree: Traverse all components in tree and accumulate all components where test(component) is true.
These methods are hard to spell. Luckily, we have another more concise ways to find DOM nodes from a React component.
findDOMNode
If a React component has been mounted into the DOM, react-dom
module's findDOMNode
method returns the corresponding native browser DOM element.
We use it to write the fourth test case. It is to test the toggle behavior when a user clicks the Todo item.
import {findDOMNode} from 'react-dom';
describe('DOM Rendering', function (done) {
it('When click the Todo item,it should become done', function () {
const app = TestUtils.renderIntoDocument(<App/>);
const appDOM = findDOMNode(app);
const todoItem = appDOM.querySelector('li:first-child span');
let isDone = todoItem.classList.contains('todo-done');
TestUtils.Simulate.click(todoItem);
expect(todoItem.classList.contains('todo-done')).to.be.equal(!isDone);
});
});
In the code above, findDOMNode
method returns App
's DOM node. Then we find out the first li
element in it, and simulate a click action upon it. Last, we expect the todo-done
class in todoItem.classList
to toggle.
The fifth test case is to test adding a new Todo item.
describe('DOM Rendering', function (done) {
it('Add an new Todo item, when click the new todo button', function () {
const app = TestUtils.renderIntoDocument(<App/>);
const appDOM = findDOMNode(app);
let todoItemsLength = appDOM.querySelectorAll('.todo-text').length;
let addInput = appDOM.querySelector('input');
addInput.value = 'Todo four';
let addButton = appDOM.querySelector('.add-todo button');
TestUtils.Simulate.click(addButton);
expect(appDOM.querySelectorAll('.todo-text').length).to.be.equal(todoItemsLength + 1);
});
});
In the code above, at first, we find the input
box and add a value into it. Then, we find the Add Todo
button and simulate the click action upon it. Last, we expect the new Todo item to be appended into the Todo list.
Enzyme Library
Enzyme is a wrapper library of official Test Utilities, mimicking jQuery's API to provide an intuitive and flexible way to test React component.
It provides three ways to do the testing.
shallow
render
mount
shallow
shallow is a wrapper of Test Utilities' shallow rendering.
The following is the first test case to test App's title.
import {shallow} from 'enzyme';
describe('Enzyme Shallow', function () {
it('App\'s title should be Todos', function () {
let app = shallow(<App/>);
expect(app.find('h1').text()).to.equal('Todos');
});
};
In the code above, shallow
method returns the shallow rendering of App
, and app.find
method returns its h1
element, and text
method returns the element's text.
Please keep in mind that .find
method only supports simple selectors. When meeting complex selectors, it returns no results.
component.find('.my-class'); // by class name
component.find('#my-id'); // by id
component.find('td'); // by tag
component.find('div.custom-class'); // by compound selector
component.find(TableRow); // by constructor
component.find('TableRow'); // by display name
render
render
is used to render React components to static HTML and analyze the resulting HTML structure. It returns a wrapper very similar to shallow
; however, render uses a third party HTML parsing and traversal library Cheerio. This means it returns a CheerioWrapper.
The following is the second test case to test the initial state of Todo items.
import {render} from 'enzyme';
describe('Enzyme Render', function () {
it('Todo item should not have todo-done class', function () {
let app = render(<App/>);
expect(app.find('.todo-done').length).to.equal(0);
});
});
In the code above, you should see, no matter a ShallowWapper or a CheerioWrapper, Enzyme provides them with the same API (find
method).
mount
mount
is the method to mount your React component into a real DOM node.
The following is the third test case to test the delete button.
import {mount} from 'enzyme';
describe('Enzyme Mount', function () {
it('Delete Todo', function () {
let app = mount(<App/>);
let todoLength = app.find('li').length;
app.find('button.delete').at(0).simulate('click');
expect(app.find('li').length).to.equal(todoLength - 1);
});
});
In the code above, find
method returns an object containing all eligible children components. at
method returns the child component at the specified position and simulate
method simulates some action upon it.
The following is the fourth test case to test the toggle behaviour of a Todo item.
import {mount} from 'enzyme';
describe('Enzyme Mount', function () {
it('Turning a Todo item into Done', function () {
let app = mount(<App/>);
let todoItem = app.find('.todo-text').at(0);
todoItem.simulate('click');
expect(todoItem.hasClass('todo-done')).to.equal(true);
});
});
The following is the fifth test case to test the Add Todo
button.
import {mount} from 'enzyme';
describe('Enzyme Mount', function () {
it('Add a new Todo', function () {
let app = mount(<App/>);
let todoLength = app.find('li').length;
let addInput = app.find('input').get(0);
addInput.value = 'Todo Four';
app.find('.add-button').simulate('click');
expect(app.find('li').length).to.equal(todoLength + 1);
});
});
API List
The following is an incomplete list of Enzyme API. It should give you a general concept of Enzyme's usage.
.get(index)
: Returns the node at the provided index of the current wrapper.at(index)
: Returns a wrapper of the node at the provided index of the current wrapper.first()
: Returns a wrapper of the first node of the current wrapper.last()
: Returns a wrapper of the last node of the current wrapper.type()
: Returns the type of the current node of the wrapper.text()
: Returns a string representation of the text nodes in the current render tree.html()
: Returns a static HTML rendering of the current node.props()
: Returns the props of the root component.prop(key)
: Returns the named prop of the root component.state([key])
: Returns the state of the root component.setState(nextState)
: Manually sets state of the root component.setProps(nextProps)
: Manually sets props of the root component
Licence
MIT