唠一唠React的组件测试

刚做完前学僧的401-react-clickable-grid,项目本身并不难,但全部实现后的测试却让我有些犯难,newraina做的也并没有携带测试。好不容易参照网上的一篇博文与相应测试工具的文档完成简单的单元测试后,我无意中发现阮一峰老师在他的github上发布的react-testing-demo,刚好可以参照一下大牛的demo,阮老师写的React TodoList也是不错的学习样例哟。我同时结合网上的一些资料,对react的组件测试做一个小小的总结,当然极大部分参照了阮老师的英文README.md哒。

首先,React最重要的测试工具就是official Test Utilities,官方测试工具集,不过它只提供了较为低级的API。因此,出现了很多第三方的测试类库,不过他们大多基于官方测试集开发。测试库Enzyme是众多同类测试库中较为容易掌握的。

Test Utilities提供给用户两种测试选择:

Shallow Rendering: 用来测试虚拟DOM对象

Shallow Rendering就仅仅渲染一层,是不对子组件进行渲染的,返回虚拟DOM对象

1
2
3
4
5
6
import TestUtils from 'react-addons-test-utils';
function shallowRender(Component) {
const renderer = TestUtils.createRenderer();
renderer.render(<Component/>);
return renderer.getRenderOutput();
}

下面我们用一个非常简单的例子来理解一下什么是浅渲染,以及常用到的API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 一个顶层组件App
class App extends React.Component {
render() {
return (
<div>
<h1>Todos</h1>
<TodoList/>
<AddTodo/>
</div>
);
}
}
// 使用chai的测试代码, 主要目的是测试<h1>Todos</h1>
describe('Shallow Rendering', function () {
it('App\'s title should be Todos', function () {
// app为浅渲染的结果
const app = shallowRender(App);
/*
app.type为最外层的div.
app.props.chiledren为div下的三个子节点,但TodoList与AddTodo不会被继续渲染,仍以<TodoList/><AddTodo/>的形式呈现.
app.props.chiledren[0]为div的第一个子节点,type为h1,同时测试h1的文本子节点(Todos).
*/
expect(app.props.children[0].type).to.equal('h1');
expect(app.props.children[0].props.children).to.equal('Todos');
});
});

当然shallowRender还可以进行扩展,传入props参数给组件,renderer.render(<Component {...props}/>)

DOM Rendering: 测试真正的DOM节点

第二种选择就是直接将React的组件渲染成真实的DOM,也就是完全渲染,使用renderIntoDocument方法。不过使用这个方法需要搭建DOM环境,这里我们采用jsdom:

1
2
3
4
5
6
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;
}

我们需要在测试启动的命令行中require这段代码,修改package.json:

1
2
3
4
5
{
"scripts": {
"test": "mocha --compilers js:babel-core/register --require ./test/setup.js",
},
}

同样,用测试一条Todo的删除功能的代码来说明问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe('DOM Rendering', function () {
it('Click the delete button, the Todo item should be deleted', function () {
// 这里的<App />与上文相同,但会被全部渲染,由于<TodoList/>、<AddTodo/>组件代码过长,就不放在这里了
const app = TestUtils.renderIntoDocument(<App/>);
// 在渲染后的app中找出所有组件实例,并且是标签名字符合'li'的DOM组件
let todoItems = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li');
// 统计'li'的数量
let todoLength = todoItems.length;
// 这里可以使用document的API,选择元素'button'
let deleteButton = todoItems[0].querySelector('button');
// Simulate 模拟点击事件
TestUtils.Simulate.click(deleteButton);
// 再次统计'li'的数量,若通过点击删除了一条Todo,则总数会减少1
let todoItemsAfterClick = TestUtils.scryRenderedDOMComponentsWithTag(app, 'li');
expect(todoItemsAfterClick.length).to.equal(todoLength - 1);
});
});

当然官方还提供了各种寻找node的方法,例如通过class、tag、type等等,详见官方说明.

官方提供的API比较长,我们可以利用react-domfindDOMNode方法。我们也用一个例子来说明,用户点击一条Todo后会有一道线划掉它,表示已经做完:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {findDOMNode} from 'react-dom';
describe('DOM Rendering', function (done) {
it('When click the Todo item,it should become done', function () {
// renderIntoDocument返回ReactComponent
const app = TestUtils.renderIntoDocument(<App/>);
// findDOMNode(component)获取到组件中真实的DOM
const appDOM = findDOMNode(app);
const todoItem = appDOM.querySelector('li:first-child span');
// 是否包含class为'todo-done'
let isDone = todoItem.classList.contains('todo-done');
// 进行点击测试,状态是否改变
TestUtils.Simulate.click(todoItem);
expect(todoItem.classList.contains('todo-done')).to.be.equal(!isDone);
});
});

这里阮大大说: findDOMNode方法的最大优点,就是支持复杂的CSS选择器。这是TestUtils本身不提供的。这里我也提出了自己的疑问,这个差异是如何体现的?querySelector()作为DOM的API,在findDOMNode和TestUtils渲染出来的DOM上操作应该是一致的吧。


在我自己的写的小项目中,我使用了Enzyme,对官方测试库进行了封装。主要提供了三种使用方法:

Shallow Rendering - shallow

它与官方测试库的浅渲染类似,只会渲染一层组件,shallow(node[, options]) => ReactWrapper

注意:.html()会将组件完全渲染后返回字符串,可以使用.shallow()再渲染子组件,.find()只支持简单选择器,.type()可返回组件的类型,.text()返回文本内容。

Full DOM Rendering - mount

全部渲染,enzyme已经利用jsdom模拟出一个浏览器的js环境。

Static Rendered Markup - render

全部渲染,render方法将React组件渲染成静态的HTML字符串,然后分析这段HTML代码的结构,返回一个对象。主要是采用了第三方HTML解析库Cheerio。

最后,说一个小插曲吧,正当我兴高采烈地写着这篇博文时,阮一峰大大也发布将README.md用中文进行整理发到了他的博客上 - React 测试入门教程,又是一篇好文!还以为阮大大只是推推github不发文呢,小忧伤,哈哈~~