React 测试入门教程

越来越多的人,使用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):设置根组件的属性

(完)

时间: 2024-10-29 20:45:11

React 测试入门教程的相关文章

超级给力的JavaScript的React框架入门教程_基础知识

 React 是 Facebook 里一群牛 X 的码农折腾出的牛X的框架. 实现了一个虚拟 DOM,用 DOM 的方式将需要的组件秒加,用不着的秒删.React 扮演着 MVC 结构中 V 的角色, 不过你要是 Flux 搭配使用, 你就有一个很牛X的能让轻松让 M 和 V 同步的框架了,Flux 的事以后再说~组件们 在 React 中,你可以创建一个有特殊功能的组件,这在 HTML 元素里你是打着灯笼也找不到的,比如这个教程里的下拉导航.每个组件都有自己的地盘(scope),所以我们定义一

Flux 架构入门教程

过去一年中,前端技术大发展,最耀眼的明星就是React. React 本身只涉及UI层,如果搭建大型应用,必须搭配一个前端框架.也就是说,你至少要学两样东西,才能基本满足需要:React + 前端框架. Facebook官方使用的是 Flux 框架.本文就介绍如何在 React 的基础上,使用 Flux 组织代码和安排内部逻辑,使得你的应用更易于开发和维护. 阅读本文之前,我假设你已经掌握了 React .如果还没有,可以先看我写的<React入门教程>.与以前一样,本文的目标是使用最简单的语

如何在Python的Flask框架中使用模版的入门教程

  如何在Python的Flask框架中使用模版的入门教程?          概述 如果你已经阅读过上一个章节,那么你应该已经完成了充分的准备工作并且创建了一个很简单的具有如下文件结构的Web应用: microblog |-flask文件夹 |-<一些虚拟环境的文件> |-app文件夹 | |-static文件夹 | |-templates文件夹 | |-__init__.py文件 | |-views.py文件 |-tmp文件夹 |-run.py文件 以上给你介绍了在Python的Flask

php中的curl使用入门教程和常见用法实例

[目录] php中的curl使用入门教程和常见用法实例 一.curl的优势 二.curl的简单使用步骤 三.错误处理 四.获取curl请求的具体信息 五.使用curl发送post请求 六.文件上传 七.文件下载 八.http 验证 九.通过代理发送请求 十.发送json数据 十一.cURL批处理(multi cURL) 十二.总结 起先cURL是做为一种命令行工具设计出来的,比较幸运的是,php也支持cURL了.通过cURL这个利器,我们能在php程序中自由地发送 HTTP请求到某个url来获取

XML入门教程:向XML文件存储数据

xml|教程|入门教程|数据 通常,我们在数据库中存储数据.不过,如果希望数据的可移植性更强,我们可以把数据存储XML文件中. 创建并保存XML文件 如果数据要被传送到非Windows平台上的应用程序,那么把数据保存在XML文件中是有好处的.请记住,XML有很强的跨平台可移植性,并且数据无需转换! 首先,我们将学习如何创建并保存一个XML文件.下面的这个XML文件将被命名为"test.xml",并被保存在服务器上的c目录中.我们将使用ASP和微软的XMLDOM对象来创建并保存这个XML

XML新手入门教程:了解学习 XML 属性

xml|教程|入门教程 与HTML类似,XML元素也可以在开始标签中包含属性. 属性被用于提供关于元素的附加信息. XML 属性 XML元素可拥有属性. 回忆一下HTML的这个标签:<IMG SRC="http://www.webjx.com/htmldata/2007-06-20/computer.gif">.SRC属性提供了关于img元素的附加信息. 在HTML(以及XML)中,属性可提供有关元素的附加(额外的)信息: <img src="http://

Dreamweaver MX 2004 入门教程之超级连接

dreamweaver|教程|入门教程 作为网站肯定有很多的页面,如果页面之间彼此是独立的,那么网页就好比是孤岛, 这样的网站是无法运行的.为了建立起网页之间的联系我们必须使用超级链接.称"超级链接 ",是因为它什么都能链接,如:网页.下载文件.网站地址.邮件地址--等等.下边我们 就来讨论怎样在网页中创建超级链接. [页面之间的超级连接] 在网页中,单击了某些图片.有下划线或有明示链接的文字就会跳转到相应的网页中 去. 1.在网页中选中要做超级链接的文字或者图片. 2.在属性面板中单

Dreamweaver MX 2004 入门教程之实用技巧

dreamweaver|技巧|教程|入门教程  空格输入的方法 (1)在编辑网页的时候,有时根据排版要求或其它原因需要输入空格,DW MX 2004提 供了对 中文输入的最好的支持.首先进行一下设置:点"编辑"菜单-->>"首选参数"-->>"常规",勾选"允许多个连续的空格".如下图: 直接敲空格键即可输入空格(半格). (2)或通过将输入法调整到全角模式敲空格键,每敲一次,输入一个空格.方法是: 打

Dreamweaver MX 2004 入门教程之发布站点(2)

dreamweaver|教程|入门教程|站点 (图片较大 请放大后查看) 不要为站点启用文件存回和取出. 单击"下一步". 单击"完成"以完成远程站点的设置. 再次单击"完成"以退出"管理站点"对话框. [上传文件] 在设置了本地文件夹和远程文件夹(空文件夹)之后,可以将文件从本地文件夹上传 到 Web 服务器. 请执行以下操作: 在"文件"面板("窗口">"文件&quo