## 三、官方测试工具库 我们知道,一个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`本身不提供的。