## 三、官方测试工具库
我们知道,一个React组件有两种存在形式:虚拟DOM对象(即`React.Component`的实例)和真实DOM节点。官方测试工具库对这两种形式,都提供测试解决方案。
* Shallow Rendering:测试虚拟DOM的方法
* DOM Rendering: 测试真实DOM的方法
### 3.1 Shallow Rendering
Shallow Rendering (浅渲染)指的是,将一个组件渲染成虚拟DOM对象,但是只渲染第一层,不渲染所有子组件,所以处理速度非常快。它不需要DOM环境,因为根本没有加载进DOM。
首先,在测试脚本之中,引入官方测试工具库。
~~~
import TestUtils from 'react-addons-test-utils';
~~~
然后,写一个 Shallow Rendering 函数,该函数返回的就是一个浅渲染的虚拟DOM对象。
~~~
import TestUtils from 'react-addons-test-utils';
function shallowRender(Component) {
const renderer = TestUtils.createRenderer();
renderer.render(<Component/>);
return renderer.getRenderOutput();
}
~~~
第一个[测试用例](https://github.com/ruanyf/react-testing-demo/blob/master/test/shallow1.test.js),是测试标题是否正确。这个用例不需要与DOM互动,不涉及子组件,所以使用浅渲染非常合适。
~~~
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');
});
});
~~~
上面代码中,`const app = shallowRender(App)`表示对`App`组件进行"浅渲染",然后`app.props.children[0].props.children`就是组件的标题。
你大概会觉得,这个属性的写法太古怪了,但实际上是有规律的。每一个虚拟DOM对象都有`props.children`属性,它包含一个数组,里面是所有的子组件。`app.props.children[0]`就是第一个子组件,在我们的例子中就是`h1`元素,它的`props.children`属性就是`h1`的文本。
第二个[测试用例](https://github.com/ruanyf/react-testing-demo/blob/master/test/shallow2.test.js),是测试`Todo`项的初始状态。
首先,需要修改`shallowRender`函数,让它接受第二个参数。
~~~
import TestUtils from 'react-addons-test-utils';
function shallowRender(Component, props) {
const renderer = TestUtils.createRenderer();
renderer.render(<Component {...props}/>);
return renderer.getRenderOutput();
}
~~~
下面就是测试用例。
~~~
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);
});
});
~~~
上面代码中,由于[`TodoItem`](https://github.com/ruanyf/react-testing-demo/blob/master/app/components/TodoItem.jsx)是[`App`](https://github.com/ruanyf/react-testing-demo/blob/master/app/components/App.jsx)的子组件,所以浅渲染必须在`TodoItem`上调用,否则渲染不出来。在我们的例子中,初始状态反映在组件的`Class`属性(`props.className`)是否包含`todo-done`。
### 3.2 renderIntoDocument
官方测试工具库的第二种测试方法,是将组件渲染成真实的DOM节点,再进行测试。这时就需要调用`renderIntoDocument` 方法。
~~~
import TestUtils from 'react-addons-test-utils';
import App from '../app/components/App';
const app = TestUtils.renderIntoDocument(<App/>);
~~~
`renderIntoDocument` 方法要求存在一个真实的DOM环境,否则会报错。因此,测试用例之中,DOM环境(即`window`, `document` 和 `navigator` 对象)必须是存在的。[jsdom](https://github.com/tmpvar/jsdom) 库提供这项功能。
~~~
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;
}
~~~
将上面这段代码,保存在`test`子目录下,取名为 [`setup.js`](https://github.com/ruanyf/react-testing-demo/blob/master/test/setup.js)。然后,修改`package.json`。
~~~
{
"scripts": {
"test": "mocha --compilers js:babel-core/register --require ./test/setup.js",
},
}
~~~
现在,每次运行`npm test`,`setup.js` 就会包含在测试脚本之中一起执行。
第三个[测试用例](https://github.com/ruanyf/react-testing-demo/blob/master/test/dom1.test.js),是测试删除按钮。
~~~
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);
});
});
~~~
上面代码中,第一步是将`App`渲染成真实的DOM节点,然后使用`scryRenderedDOMComponentsWithTag`方法找出`app`里面所有的`li`元素。然后,取出第一个`li`元素里面的`button`元素,使用`TestUtils.Simulate.click`方法在该元素上模拟用户点击。最后,判断剩下的`li`元素应该少了一个。
这种测试方法的基本思路,就是找到目标节点,然后触发某种动作。官方测试工具库提供多种方法,帮助用户找到所需的DOM节点。
* [scryRenderedDOMComponentsWithClass](https://facebook.github.io/react/docs/test-utils.html#scryrendereddomcomponentswithclass):找出所有匹配指定`className`的节点
* [findRenderedDOMComponentWithClass](https://facebook.github.io/react/docs/test-utils.html#findrendereddomcomponentwithclass):与`scryRenderedDOMComponentsWithClass`用法相同,但只返回一个节点,如有零个或多个匹配的节点就报错
* [scryRenderedDOMComponentsWithTag](https://facebook.github.io/react/docs/test-utils.html#scryrendereddomcomponentswithtag):找出所有匹配指定标签的节点
* [findRenderedDOMComponentWithTag](https://facebook.github.io/react/docs/test-utils.html#findrendereddomcomponentwithtag):与`scryRenderedDOMComponentsWithTag`用法相同,但只返回一个节点,如有零个或多个匹配的节点就报错
* [scryRenderedComponentsWithType](https://facebook.github.io/react/docs/test-utils.html#scryrenderedcomponentswithtype):找出所有符合指定子组件的节点
* [findRenderedComponentWithType](https://facebook.github.io/react/docs/test-utils.html#findrenderedcomponentwithtype):与`scryRenderedComponentsWithType`用法相同,但只返回一个节点,如有零个或多个匹配的节点就报错
* [findAllInRenderedTree](https://facebook.github.io/react/docs/test-utils.html#findallinrenderedtree):遍历当前组件所有的节点,只返回那些符合条件的节点
可以看到,上面这些方法很难拼写,好在还有另一种找到DOM节点的替代方法。
### 3.3 findDOMNode
如果一个组件已经加载进入DOM,`react-dom`模块的`findDOMNode`方法会返回该组件所对应的DOM节点。
我们使用这种方法来写第四个[测试用例](https://github.com/ruanyf/react-testing-demo/blob/master/test/dom2.test.js),用户点击Todo项时的行为。
~~~
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);
});
});
~~~
上面代码中,`findDOMNode`方法返回`App`所在的DOM节点,然后找出第一个`li`节点,在它上面模拟用户点击。最后,判断`classList`属性里面的`todo-done`,是否出现或消失。
第五个测试用例,是添加新的Todo项。
~~~
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);
});
});
~~~
上面代码中,先找到`input`输入框,添加一个值。然后,找到`Add Todo`按钮,在它上面模拟用户点击。最后,判断新的Todo项是否出现在Todo列表之中。
`findDOMNode`方法的最大优点,就是支持复杂的CSS选择器。这是`TestUtils`本身不提供的。