Getting started with React testing
More and more people use React to develop web applications. Its testing becomes a big problem.
React’s component structure and JSX syntax are not suitable for traditional testing tools, and new testing methods and tools must be available.
This article summarizes the current basic practices and best practices of React testing, and teaches you how to write React tests.
1. Demo application
Please install Demo first .
$ git clone https://github.com/ruanyf/react-testing-demo.git $ cd react-testing-demo && npm install $ npm start
Then, open http://127.0.0.1:8080/, and you will see a Todo application.
Next, we will test this application, there are 5 test points in total.
- App title should be “Todos”
- The initial state of the Todo item (“incomplete” or “completed”) should be correct
- Click on a Todo item and it will reverse the status (“incomplete” becomes “completed” and vice versa)
- Click the delete button, the Todo item will be deleted
- Click the Add button, a Todo item will be added
These 5 test cases have been written, and you can see the results after you execute them.
$ npm test
Let’s take a look at how to write test cases. I use Mocha as the test framework. If you are not familiar with it, you can read the “Mocha Tutorial” written by me first .
Second, the test tool library
React testing must use the official testing tool library , but it is not convenient to use, so some people have done encapsulation and launched some third-party libraries. Among them, Airbnb’s Enzyme is the easiest to use.
This means that there are at least two ways to write the same test case, both of which will be introduced in this article.
- The wording of the official test tool library
- Enzyme’s writing
Third, the official test tool library
We know that a React component has two forms of existence: virtual DOM objects (ie
React.Component
instances) and real DOM nodes.
The official test tool library provides test solutions for both forms.
- Shallow Rendering : Method of testing virtual DOM
- DOM Rendering : A method to test the real DOM
3.1 Shallow Rendering
Shallow Rendering refers to rendering a component as a virtual DOM object, but only the first layer is rendered, not all sub-components, so the processing speed is very fast. It does not need a DOM environment because it is not loaded into the DOM at all.
First of all, in the test script, introduce the official test tool library.
import TestUtils from 'react-addons-test-utils';
Then, write a Shallow Rendering function, which returns a shallow rendered virtual DOM object.
import TestUtils from 'react-addons-test-utils'; function shallowRender(Component) { const renderer = TestUtils.createRenderer(); renderer.render(<Component/>); return renderer.getRenderOutput(); }
The first test case is to test whether the title is correct. This use case does not require interaction with the DOM and does not involve sub-components, so shallow rendering is very suitable.
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'); }); });
In the above code, it
const app = shallowRender(App)
means
App
“shallow rendering”
app.props.children[0].props.children
of the component
, and then
the title of the component.
You might think that the writing of this attribute is too weird, but it is actually regular.
Every virtual DOM object has
props.children
attributes, it contains an array, which contains all the sub-components.
app.props.children[0]
It is the first child component, in our case it is the
h1
element, and its
props.children
attribute is
h1
the text.
The second
test case
is
Todo
the initial state
of the test
item.
First, you need to modify the
shallowRender
function so that it accepts the 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 above code, because it
TodoItem
is
App
a child component, the shallow rendering must be
TodoItem
called on it, otherwise it will not be rendered.
In our example, the initial state is reflected in
whether the
component’s
Class
property (
props.className
) is included
todo-done
.
3.2 renderIntoDocument
The second test method of the official test tool library is to render the component into a real DOM node and then perform the test.
At this time, the
renderIntoDocument
method
needs to be called
.
import TestUtils from 'react-addons-test-utils'; import App from '../app/components/App'; const app = TestUtils.renderIntoDocument(<App/>);
renderIntoDocument
The method requires a real DOM environment, otherwise an error will be reported.
Therefore, among the test cases, DOM environment (that is
window
,
document
and
navigator
the object) must be present.
The jsdom
library provides this functionality.
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; }
Save the above code in a
test
subdirectory and name it
setup.js
.
Then, modify
package.json
.
{ "scripts": { "test": "mocha --compilers js:babel-core/register --require ./test/setup.js", }, }
Now, every time it is run
npm test
,
setup.js
it will be included in the test script and executed 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 above code, the first step is to
App
render into a real DOM node, and then use the
scryRenderedDOMComponentsWithTag
method to find
app
all the
li
elements
inside
.
Then, take out the
li
element in the
first
button
element and use the
TestUtils.Simulate.click
method to simulate the user’s click on the element.
Finally, judge that the remaining
li
elements should be missing by one.
The basic idea of this test method is to find the target node and then trigger a certain action. The official testing tool library provides a variety of methods to help users find the DOM nodes they need.
- scryRenderedDOMComponentsWithClass : find all
className
nodes that match the specified- findRenderedDOMComponentWithClass : Same as
scryRenderedDOMComponentsWithClass
usage, but only returns one node. If there are zero or more matching nodes, an error will be reported- scryRenderedDOMComponentsWithTag : find all nodes that match the specified tag
- findRenderedDOMComponentWithTag : Same as
scryRenderedDOMComponentsWithTag
usage, but only returns one node. If there are zero or more matching nodes, an error will be reported- scryRenderedComponentsWithType : find all nodes that match the specified subcomponent
- findRenderedComponentWithType : Same as
scryRenderedComponentsWithType
usage, but only returns one node. If there are zero or more matching nodes, an error will be reported- findAllInRenderedTree : Traverse all the nodes of the current component and return only those nodes that meet the conditions
As you can see, these methods are difficult to spell, but fortunately, there is another alternative method to find DOM nodes.
3.3 findDOMNode
If a component has been loaded into the DOM,
react-dom
the
findDOMNode
method of the
module
will return the DOM node corresponding to the component.
We use this method to write the fourth test case , the behavior when the 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 above code, the
findDOMNode
method returns
App
the DOM node where it is located, and then finds the first
li
node and simulates the user’s click on it.
Finally, determine whether the
classList
attribute
todo-done
appears or disappears.
The fifth test case is to add 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 above code, first find the
input
input box and add a value.
Then, find the
Add Todo
button and simulate the user clicking on it.
Finally, determine whether the new Todo item appears in the Todo list.
findDOMNode
The biggest advantage of the method is that it supports complex CSS selectors.
This is
TestUtils
not provided by itself.
Four, Enzyme library
Enzyme is a package of the official testing tool library, it simulates the jQuery API, very intuitive, easy to use and learn.
It provides three test methods.
shallow
render
mount
4.1 shallow
The shallow method is the encapsulation of the official shallow rendering.
Below is the first
test case
,
App
the title of the
test
.
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 above code, the
shallow
method returns
App
the shallow rendering, then the
app.find
method finds the
h1
element, and the
text
method takes out the text of the element.
Regarding the
find
method, one note is that it only supports simple selectors, not the slightly more complicated CSS selectors.
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
4.2 render
render
The method renders the React component into a static HTML string, then analyzes the structure of this HTML code and returns an object.
It
shallow
is very
similar to the
method. The main difference is that it uses the third-party HTML parsing library Cheerio, which returns a Cheerio instance object.
The following is the second test case to test the initial state of all 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 above code, you can see that the
render
method and
shallow
method API are basically the same.
Enzyme’s design is to allow different underlying processing engines to have the same API (such as
find
methods).
4.3 mount
mount
Method is used to load React components as real DOM nodes.
Below is the third test case , testing 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 above code, the
find
method returns an object that contains all sub-components that meet the conditions.
Based on it, the
at
method returns the child component at the specified position, and the
simulate
method triggers a certain behavior on this component.
The following is the fourth test case to test the click behavior of Todo items.
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 adding a new Todo item.
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); }); });
4.4 API
The following is part of the API of Enzyme, from which you can understand its general usage.
.get(index)
: Returns the DOM node of the child component at the specified position.at(index)
: Return the subcomponent at the specified position.first()
: Return to the first subcomponent.last()
: Return to the last subcomponent.type()
: Returns the type of the current component.text()
: Returns the text content of the current component.html()
: Returns the HTML code form of the current component.props()
: Return all attributes of the root component.prop(key)
: Return the specified attributes of the root component.state([key])
: Return the status of the root component.setState(nextState)
: Set the state of the root component.setProps(nextProps)
: Set the properties of the root component
(over)