A tutorial of testing React components
This repo shows you how to test React component. It is loosely based on Jack Franklin's article "Testing React Applications".
$ 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 testto find the test result.
$ npm test
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.
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 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(); return renderer.getRenderOutput(); }
In the code above, we define a function
shallowRenderto 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.childrenintimidating, but it is not. Each virtual DOM object has a
props.childrenproperty which contains its all children components.
app.props.children[0]is the
h1element whose
props.childrenis the text of
h1.
The second test case is to test the initial state of a
TodoItemis undone.
At first, we should modify the function
shallowRenderto accept second parameter.
import TestUtils from 'react-addons-test-utils';function shallowRender(Component, props) { const renderer = TestUtils.createRenderer(); renderer.render(); 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); }); });
TodoItemis a child component of
App, we have to call
shallowRenderfunction with
TodoItem, otherwise it will not be rendered. In our demo, if the state of a
TodoItemis undone, the
classproperty (
props.className) contains no
todo-done.
The second testing choice of official Test Utilities is to render a React component into a real DOM node.
renderIntoDocumentmethod is used for this purpose.
import TestUtils from 'react-addons-test-utils'; import App from '../app/components/App';const app = TestUtils.renderIntoDocument();
renderIntoDocumentmethod requires a DOM, otherwise throws an error. Before running the test case, DOM environment (includes
window,
documentand
navigatorObject) should be available. So we use jsdom to implement the DOM environment.
import jsdom from 'jsdom';if (typeof document === 'undefined') { global.document = jsdom.jsdom(''); global.window = document.defaultView; global.navigator = global.window.navigator; }
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.jswill 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(); 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,
scryRenderedDOMComponentsWithTagmethod finds all
lielements of the
appcomponent. Next, get out
todoItems[0]and find the delete button from it. Then use
TestUtils.Simulate.clickto simulate the click action upon it. Last, expect the new number of all
lielements 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.
If a React component has been mounted into the DOM,
react-dommodule's
findDOMNodemethod 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(); 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,
findDOMNodemethod returns
App's DOM node. Then we find out the first
lielement in it, and simulate a click action upon it. Last, we expect the
todo-doneclass in
todoItem.classListto 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(); 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
inputbox and add a value into it. Then, we find the
Add Todobutton and simulate the click action upon it. Last, we expect the new Todo item to be appended into the Todo list.
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 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(); expect(app.find('h1').text()).to.equal('Todos'); }); };
In the code above,
shallowmethod returns the shallow rendering of
App, and
app.findmethod returns its
h1element, and
textmethod returns the element's text.
Please keep in mind that
.findmethod 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
renderis 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(); 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 (
findmethod).
mountis 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(); 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,
findmethod returns an object containing all eligible children components.
atmethod returns the child component at the specified position and
simulatemethod 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(); 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 Todobutton.
import {mount} from 'enzyme';describe('Enzyme Mount', function () { it('Add a new Todo', function () { let app = mount(); 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); }); });
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
MIT