分类

React 测试入门教程

作者: 阮一峰

日期: 2016年2月13日

越来越多的人,使用React开发Web应用。它的测试就成了一个大问题。

React的组件结构和JSX语法,不适用传统的测试工具,必须有新的测试方法和工具。

本文总结目前React测试的基本做法和最佳实践,手把手教你如何写React测试。

一、Demo应用

请先安装Demo


$ git clone https://github.com/ruanyf/react-testing-demo.git
$ cd react-testing-demo && npm install
$ npm start

然后,打开 http://127.0.0.1:8080/,你会看到一个 Todo 应用。

接下来,我们就要测试这个应用,一共有5个测试点。

  1. 应用标题应为"Todos"
  2. Todo项的初始状态("未完成"或"已完成")应该正确
  3. 点击一个Todo项,它就反转状态("未完成"变为"已完成",反之亦然)
  4. 点击删除按钮,该Todo项就被删除
  5. 点击添加按钮,会新增一个Todo项

这5个测试用例都已经写好了,执行一下就可以看到结果。


$ npm test

下面就来看,测试用例应该怎么写。测试框架我用的是Mocha,如果你不熟悉,可以先读我写的《Mocha教程》

二、测试工具库

React测试必须使用官方的测试工具库,但是它用起来不够方便,所以有人做了封装,推出了一些第三方库,其中Airbnb公司的Enzyme最容易上手。

这就是说,同样的测试用例至少有两种写法,本文都将介绍。

  • 官方测试工具库的写法
  • Enzyme的写法

三、官方测试工具库

我们知道,一个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();
}

第一个测试用例,是测试标题是否正确。这个用例不需要与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的文本。

第二个测试用例,是测试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);
  });
});

上面代码中,由于TodoItemApp的子组件,所以浅渲染必须在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, documentnavigator 对象)必须是存在的。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。然后,修改package.json


{
  "scripts": {
    "test": "mocha --compilers js:babel-core/register --require ./test/setup.js",
  },
}

现在,每次运行npm testsetup.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节点。

可以看到,上面这些方法很难拼写,好在还有另一种找到DOM节点的替代方法。

3.3 findDOMNode

如果一个组件已经加载进入DOM,react-dom模块的findDOMNode方法会返回该组件所对应的DOM节点。

我们使用这种方法来写第四个测试用例,用户点击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本身不提供的。

四、Enzyme库

Enzyme是官方测试工具库的封装,它模拟了jQuery的API,非常直观,易于使用和学习。

它提供三种测试方法。

  • shallow
  • render
  • mount

4.1 shallow

shallow方法就是官方的shallow rendering的封装。

下面是第一个测试用例,测试App的标题。


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');
  });
};

上面代码中,shallow方法返回App的浅渲染,然后app.find方法找出h1元素,text方法取出该元素的文本。

关于find方法,有一个注意点,就是它只支持简单选择器,稍微复杂的一点的CSS选择器都不支持。


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方法将React组件渲染成静态的HTML字符串,然后分析这段HTML代码的结构,返回一个对象。它跟shallow方法非常像,主要的不同是采用了第三方HTML解析库Cheerio,它返回的是一个Cheerio实例对象。

下面是第二个测试用例,测试所有Todo项的初始状态。


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);
  });
});

在上面代码中,你可以看到,render方法与shallow方法的API基本是一致的。 Enzyme的设计就是,让不同的底层处理引擎,都具有同样的API(比如find方法)。

4.3 mount

mount方法用于将React组件加载为真实DOM节点。

下面是第三个测试用例,测试删除按钮。


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);
  });
});

上面代码中,find方法返回一个对象,包含了所有符合条件的子组件。在它的基础上,at方法返回指定位置的子组件,simulate方法就在这个组件上触发某种行为。

下面是第四个测试用例,测试Todo项的点击行为。


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);
  });
});

下面是第五个测试用例,测试添加新的Todo项。


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

下面是Enzyme的一部分API,你可以从中了解它的大概用法。

  • .get(index):返回指定位置的子组件的DOM节点
  • .at(index):返回指定位置的子组件
  • .first():返回第一个子组件
  • .last():返回最后一个子组件
  • .type():返回当前组件的类型
  • .text():返回当前组件的文本内容
  • .html():返回当前组件的HTML代码形式
  • .props():返回根组件的所有属性
  • .prop(key):返回根组件的指定属性
  • .state([key]):返回根组件的状态
  • .setState(nextState):设置根组件的状态
  • .setProps(nextProps):设置根组件的属性

(完)

留言(36条)

能不能举几个例子看看哪些网站使用了react来构建

findDOMNode方法的最大优点,就是支持复杂的CSS选择器。这是TestUtils本身不提供的。
这是如何体现的?querySelector()是DOM的API,在findDOMNode和TestUtils渲染的DOM上操作应该是一致的吧

有点难懂!

这里有个笔误。

因为根本加载进DOM…

@think2011:谢谢指出,已经改正。

@aphper http://ant.design/ ,和『我的淘宝』。

现在大家都会用webpack进行应用的模块化和打包。
如果一个组件,它引用了样式,如: import '../style/component.scss'
这个时候,内部的 @import base.scss 是无法被babel识别的
这个时候该如何进行组件的单元测试?

当然如果直接使用className,就不会有这个问题。但是那个库已经有一定规模,并不好为了测试重做。

@阮一峰:

nice! thanks a lot.

非常感谢,作者的每篇文章都是那么精彩,让我入门了好多新的知识。

有个问题想问下作者,react组件里面有引入css文件(require('../main.css'))时,测试就会报错,不知道有没有什么解决办法?

@ZhangJianYing:

见上面评论的链接。

您好,想請教一下。
我剛開始接觸NodeJS、React 。
看了很多Tutorial,
但還不太清楚如何循序漸進的學習,
有點像是東學一點、西學一點,沒辦法並用。
可以給一些建言嗎,謝謝老師!

做到npm install就出错了。请问怎么样解决呢?

webpack: bundle is now VALID.

/root/react-testing-demo/node_modules/open-browser-webpack-plugin/index.js:44
if (err) throw err;
^
Error: Command failed: /root/react-testing-demo/node_modules/open-browser-webpack-plugin/node_modules/open/vendor/xdg-open: 1: /root/react-testing-demo/node_modules/open-browser-webpack-plugin/node_modules/open/vendor/xdg-open: xdg-mime: not found
xdg-open: no method available for opening 'http://localhost:8080'

@Edward:

打开`package.json`,删除下面这一行

"open-browser-webpack-plugin": "0.0.1",

然后,打开webpack.config.js,删除下面两行

var OpenBrowserPlugin = require('open-browser-webpack-plugin');

new OpenBrowserPlugin({ url: 'http://localhost:8080' })

然后,重新安装。

阮老师,可否写一篇关于react-native的路由设计思路的blog

[windows 10, npm 3.8.9]

npm install 安装时出现的警告:
npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
npm WARN notsup Not compatible with your operating system or architecture: [email protected]

npm start后出的错误:
./bundle.js 295 kB 0 [emitted] main
chunk {0} ./bundle.js (main) 216 kB [rendered]
[0] multi main 76 bytes {0} [built] [1 error]
.
.
.
[74] (webpack)/hot/dev-server.js 1.85 kB {0} [built]
[75] (webpack)/hot/log-apply-result.js 813 bytes {0} [built]

ERROR in (0 , _typeof3.default) is not a function
@ multi main
webpack: bundle is now VALID.

引用aphper的发言:

能不能举几个例子看看哪些网站使用了react来构建

faceBook

感谢老师!我想问问有关于组合了redux、webpack code split的组件测试要如何做呢

请问老师,如果程序里有ajax请求,那异步回调后的逻辑还可以测试吗?

引用阮一峰的发言:

@Yika:

看这里 http://stackoverflow.com/questions/32683440/handle-webpack-css-imports-when-testing-with-mocha

这个是什么原理呢?能简单讲下吗?

引用大菠萝的发言:

请问老师,如果程序里有ajax请求,那异步回调后的逻辑还可以测试吗?

mocha本身就支持callback和promise测试的,callback就直接在回调函数里写,promise的话需要return promise对象。

我是新手,react-native的技术学习了一些,包括Redux等等。但是测试这个地方一直不敢看。如果要起步,从这一篇开始是不是可行?
react-redux这篇文章写得非常好,流程很清晰。我曾经想画一张图,就是一个开口的罐子(container),包围着ui 组件。 redux在罐子外面。UI组件需要的props通过罐子口(connect)注入。 体现的思想就是redux处理状态的逻辑和UI的动作其实是解耦和的。
其他关于redux的内容还有immutable.js,normalize,reselect等等,这些内容会不会继续写?还有就是一个最最重要的话题,state如何设计规划的问题。

能针对typescript+react开发做一个教程吗?

最近开始研究mocha测试react,采用了Enzyme在使用过程中遇到以下几个问题:import的zepto在test中报错,在os中将this=>window解决了但在windows中并没用
类似TypeError:can't read propertype 'createElement'/'getComputeStyle' 不知道在windows如何解决这个问题?
login中有用到localStorage所以同样的can't read propertype 'localStorage'?
不知道问题的原因是什么?
对于什么情况使用浏览器环境运行还是node运行测试有一些疑惑,希望老师还有各位道友能解惑下

好吧 现在有了解决方案 我需要引入karma 增加window

我安装了demo 但是webpackage的web服务运行不起来啊

老师,你好,有个问题问下;现在是使用webpack进行应用的模块化和打包,如果一个组件index.jsx,里面引入了其他的一个Login.jsx组件,Login.jsx需要import进来的,假如是这样引入的 import Login from 'modules/Login';然后测试的时候,会报找不到Error: Cannot find module 'modules/Login';请问该怎么处理下 ,谢谢

老师你好,假如使用了jest+enzyme测试工具测试
import style from './base.css'
引用了style.testCss的样式,
打印wrapper.debug()的时候,输出了className={{undefined}},该怎么办

引用pandeng的发言:

老师,你好,有个问题问下;现在是使用webpack进行应用的模块化和打包,如果一个组件index.jsx,里面引入了其他的一个Login.jsx组件,Login.jsx需要import进来的,假如是这样引入的 import Login from 'modules/Login';然后测试的时候,会报找不到Error: Cannot find module 'modules/Login';请问该怎么处理下 ,谢谢

不知道你解决了没呢 ,我也遇到这个问题, 会报import的错

阮老师,Enzyme库 shallow, render, mount的解释我看懂了,但是,没有明白他们具体的使用场景,在什么情况下,我不应该使用shallow或, render或mount 呢?

官方文档已将测试工具类移入react-dom/test-utils,渲染方法移入react-test-renderer;

阮老师,很多内容都陈旧了,要你一个人来更新等于在做重复,浪费时间,建议你的博客也可以让大伙拉请求,利用大伙的力量来更新,你可以招一个管理员来审核就是了,

引用pandeng的发言:

老师,你好,有个问题问下;现在是使用webpack进行应用的模块化和打包,如果一个组件index.jsx,里面引入了其他的一个Login.jsx组件,Login.jsx需要import进来的,假如是这样引入的 import Login from 'modules/Login';然后测试的时候,会报找不到Error: Cannot find module 'modules/Login';请问该怎么处理下 ,谢谢

试试这样写
import Login from './modules/Login'

阮老师,想问一下describe, it, context等等这些术语属于什么知识呢,看什么能比较系统地了解这些术语?

react hook 的 测试方案是不是可以补充下 谢谢

我要发表看法

«-必填

«-必填,不公开

«-我信任你,不会填写广告链接