赵走x博客
网站访问量:151534
首页
书籍
软件
工具
古诗词
搜索
登录
49、Flux:理念、回顾Whinepad
48、lint、Flow、测试与复验:测试
47、lint、Flow、测试与复验:Flow
46、lint、Flow、测试与复验:ESLint
45、lint、Flow、测试与复验:package.json
44、构建实例应用:<Whinepad>
43、构建实例应用:应用配置
43、构建实例应用:<Excel>:改进的新版本
42、构建实例应用:组件:对话框
41、构建实例应用:组件:Actions
39、构建实例应用:表单:Form
38、构建实例应用:表单:<FormInput>“工厂组件”
37、构建实例应用:表单:Rating组件
36、构建实例应用:表单:Suggest
35、构建实例应用:Button组件
34、构建实例应用:组件
33、构建实例应用:Whinepad v.0.0.1
32、发布
31、开始构建
30、安装必备工具
29、为应用开发做准备:一个模板应用
28、JSX 和表单
27、JSX 和HTML 的区别
26、在JSX 中返回多个节点
25、展开属性
24、HTML 实体
23、JSX入门
22、Excel:一个出色的表格组件:下载表格数据
21、Excel:一个出色的表格组件:即时回放
20、Excel:一个出色的表格组件:搜索
19、Excel:一个出色的表格组件:编辑数据
18、Excel:一个出色的表格组件:排序
17、Excel:一个出色的表格组件
16、 PureRenderMixin
15、 性能优化:避免组件更新
14、 生命周期示例:使用子组件
13、组件生命周期示例:使用mixin
12、组件:生命周期方法
11、中途改变属性
10、从外部访问组件
9、在初始化state 时使用props:一种反模式
8、 props 与state
7、关于DOM 事件的说明
6、组件:带状态的文本框组件
5、组件的state
4、组件的propTypes
3、组件的属性
2、组件的基础
1、Hello World
50、Flux:Store
48、lint、Flow、测试与复验:测试
资源编号:76102
书籍
React快速上手开发
热度:99
要确保稳健地改进应用,接下来需要关注自动化测试。提到测试这个话题,依然有很多开源项目可供选择。React 库使用了Jest 工具(http://facebook.github.io/jest/ )进行测试,因此我们选择介绍Jest,看它对我们有什么帮助。此外,React 还提供了一个名为reactaddons-test-utils 的插件包,可以配合你进行测试工作。 首先进行安装配置。
要确保稳健地改进应用,接下来需要关注自动化测试。提到测试这个话题,依然有很多开源项目可供选择。React 库使用了Jest 工具(http://facebook.github.io/jest/ )进行测试,因此我们选择介绍Jest,看它对我们有什么帮助。此外,React 还提供了一个名为reactaddons-test-utils 的插件包,可以配合你进行测试工作。 首先进行安装配置。 # 1、安装 安装Jest 的命令行界面: ``` $ npm i -g jest-cli ``` 此外还需安装babel-jest(让你可以使用ES6 风格编写测试)以及React 的测试工具包: ``` $ npm i --save-dev babel-jest react-addons-test-utils ``` 接下来,更新package.json: ``` { /* ... */ "eslintConfig": { /* ... */ "env": { "browser": true, "jest": true }, /* ... */ "scripts": { "watch": "watch \"sh scripts/build.sh\" js/source js/__tests__ css/", "test": "jest" }, "jest": { "scriptPreprocessor": "node_modules/babel-jest", "unmockedModulePathPatterns": [ "node_modules/react", "node_modules/react-dom", "node_modules/react-addons-test-utils", "node_modules/fbjs" ] } } ``` 现在你可以在命令行直接运行Jest: ``` $ jest testname.js ``` 也可以通过npm 运行: ``` $ npm test testname.js ``` Jest 会在名为`__tests__` 的文件夹中寻找测试用例,因此我们在`js/__tests__ `目录中编写测试。 最后修改构建脚本,每次构建时都需要运行lint 以及测试: ``` # QA eslint js/source js/__tests__ flow npm test ``` 还要修改watch.sh,以监听测试目录中的文件更改(别忘了我们在package.json 文件中重复编写过这个功能): ``` watch "sh scripts/build.sh" js/source js/__tests__ css/ ``` # 2、首个测试 Jest 是基于流行的测试框架Jasmine 构建的,而Jasmine 的API 命名比较口语化,简洁易懂。一开始,你需要通过describe('suite', callback) 定义测试套件(test suite),在套件中通过it('test name', callback) 定义一个或多个测试用例(test spec),并且在每一个用例中通过expect() 函数进行断言(assertion)。 整体而言,其基础骨架大致如下: ``` describe('A suite', () => { it('is a spec', () => { expect(1).toBe(1); }); }); ``` 运行该测试: ``` $ npm test js/__tests__/dummy-test.js > whinepad@2.0.0 test /Users/stoyanstefanov/reactbook/whinepad2 > jest "js/__tests__/dummy-test.js" Using Jest CLI v0.8.2, jasmine1 PASS js/__tests__/dummy-test.js (0.206s) 1 test passed (1 total in 1 test suite, run time 0.602s) ``` 当你的测试中存在错误的断言时,比如: ``` expect(1).toBeFalsy(); ``` 测试程序会运行失败,并给出错误信息,如图7-1 所示。  图7-1:测试运行失败 # 3、首个React 测试 接下来介绍Jest 在React 世界中的应用,你可以先从测试一个简单的DOM 按钮开始。首先导入依赖: ``` import React from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-addons-test-utils'; ``` 设置测试套件: ``` describe('We can render a button', () => { it('changes the text after click', () => { // ... }); }); ``` 既然模板代码准备好了,接下来就要开始进行渲染和测试了。首先渲染一些简单的JSX: ``` const button = TestUtils.renderIntoDocument(
ev.target.innerHTML = 'Bye'}> Hello
); ``` 在这里,我们使用了React 的测试工具库渲染JSX——当你点击按钮时,文本内容会发生改变。 在内容渲染完成后,需要检查渲染内容是否符合设想: ``` expect(ReactDOM.findDOMNode(button).textContent).toEqual('Hello'); ``` 如你所见,这里使用了ReactDOM.findDOMNode() 获取DOM 节点。随后,你可以使用熟悉的DOM API 检查该节点。 你通常还需要测试界面与用户的交互。React 提供了TestUtils.Simulate 对象,方便你完成这件事情: ``` TestUtils.Simulate.click(button); ``` 最后需要检查界面是否响应了交互事件: ``` expect(ReactDOM.findDOMNode(button).textContent).toEqual('Bye'); ``` 本章的剩余部分将会介绍更多例子和API,其中主要用到的工具如下: * TestUtils.renderIntoDocument(用于渲染任意JSX); * TestUtils.Simulate.* 负责与界面进行交互; * ReactDOM.findDOMNode()(或者其他一些TestUtils 方法)负责取得DOM 节点的引用,然后检查节点内容是否符合设想。 # 4、测试Button 组件 `
`组件的代码如下所示: ``` /* @flow */ import React from 'react'; import classNames from 'classnames'; type Props = { href: ?string, className: ?string, }; const Button = (props: Props) => props.href ?
:
export default Button ``` 我们将测试如下功能: * 根据href 属性是否存在,对应渲染`
` 标签或`
` 标签(第一个测试用例); * 接收自定义类名(第二个测试用例)。 创建一个新的测试文件: ``` jest .dontMock('../source/components/Button') .dontMock('classnames') ; import React from 'react'; import ReactDOM from 'react-dom'; import TestUtils from 'react-addons-test-utils'; ``` 这里的import 语句没有变化,但前面加上了新的jest.dontMock() 调用。 这里mock 的含义是指使用某些模拟的代码替代原有的功能,以便进行测试。mock 在单元测试中很常用,因为你希望测试的是一个“单元”——隔离测试某个模块,以减少整个系统中其他模块的影响。人们通常要花费相当多的努力在编写mock 上,而Jest 则采取了相反的做法:自动为每个依赖的模块生成mock,并默认提供mock。当你希望测试真实代码而不是mock 的时候,可以通过dontMock() 方法设置不需要进行mock。 在上述例子中,你声明了不希望mock `
`组件及其使用的classnames 库。 接下来引入`
` 组件: ``` const Button = require('../source/components/Button'); ``` 在本书编写时,尽管在Jest 文档中也采用这种写法,但上述require() 调用不能正常工作。你需要将其修改为: ``` const Button = require('../source/components/Button').default; ``` >注1:从Jest 15.0 版本开始,自动mock 的功能已经被禁用了,具体说明参见http://facebook.github.io/jest/blog/2016/09/01/jest-15.html#disabled-automocking 。——译者注 import 语句也不能生效: ``` import Button from '../source/components/Button'; ``` 需要修改为: ``` import _Button from '../source/components/Button'; const Button = _Button.default; ``` 另一种做法是在`
`组件中把 ``` export default Button ``` 修改为 ``` export {Button} ``` 然后通过 ``` import {Button} from '../source/component/Button' ``` 进行导入。 希望在读者阅读本书时,默认的import 语句已经可以正常工作。 ### 1. 第一个用例 接下来,我们分别使用describe() 方法和it() 方法设置测试套件和第一个测试用例: ``` describe('Render Button components', () => { it('renders
vs
', () => { /* 渲染组件并检测结果 */ }); }); ``` 我们先渲染一个简单的按钮组件。它没有href 属性,因此实际应该渲染一个`
` 标签: ``` const button = TestUtils.renderIntoDocument(
Hello
); ``` 注意,`
`这种无状态函数式组件需要包裹在另一层DOM 节点中,以便随后通过ReactDOM 获取。 现在调用ReactDOM.findDOMNode(button) 会返回包裹层`
`。因此要获取`
`,需要取得第一个子节点,并检查确认该节点是否为按钮: ``` expect(ReactDOM.findDOMNode(button).children[0].nodeName).toEqual('BUTTON'); ``` 类似地,你还需要在这个测试用例中验证当href 属性存在时是否会渲染
标签。 ``` const a = TestUtils.renderIntoDocument(
Hello
); expect(ReactDOM.findDOMNode(a).children[0].nodeName).toEqual('A'); ``` ### 2. 第二个用例 在第二个测试用例中,你需要添加自定义类名,并检查最终生成的类名是否符合预期: ``` it('allows custom CSS classes', () => { const button = TestUtils.renderIntoDocument(
Hello
); const buttonNode = ReactDOM.findDOMNode(button).children[0]; expect(buttonNode.getAttribute('class')).toEqual('Button good bye'); }); ``` 这里有必要重点强调Jest 的mock 功能。有时,你可能在编写测试时发现没有得到预期的结果。这种情况有可能是因为你忘记关闭了Jest 的mock 功能。比如,你可以尝试把顶部的代码改为: ``` jest .dontMock('../source/components/Button') // .dontMock('classnames') ; ``` 这时Jest 会mock 你的classnames 模块,并使得该模块不会实现任何功能。你可以通过以下测试证明这个结果: ``` const button = TestUtils.renderIntoDocument(
Hello
); console.log(ReactDOM.findDOMNode(button).outerHTML); ``` 这段代码在控制台中生成的HTML 代码如下: ```
Hello
``` 如你所见,无论填写什么类名,最终都不会生成出来,因为classNames() 在mock 之后不会实现任何功能。 把注释掉的dontMock() 方法还原: ``` jest .dontMock('../source/components/Button') .dontMock('classnames') ; ``` 然后outerHTML 就会发生变化: ```
Hello
``` 这时你的测试就可以成功通过了。 当一个测试的执行不正常时,你可能需要知道实际生成的HTML 结构是什么。一个快速简便的方法就是使用console.log(node.outerHTML),让HTML内容在控制台中输出。 # 5、测试Actions 组件 `
` 组件也是一个无状态组件,这意味着你需要把它包裹在另一层DOM 节点中,以便随后进行检查。一种方式是像前面对`
`组件所做的那样,将其包裹在div 中并通过以下方式访问: ``` const actions = TestUtils.renderIntoDocument(
); ReactDOM.findDOMNode(actions).children[0]; //
组件的根节点 ``` ### 1. 组件包裹层 另一种方式是使用组件包裹层,以方便你随后使用TestUtils 提供的各种方法寻找需要检查的节点。 这个包裹层非常简单,可以定义在一个模块中,方便以后重用: ``` import React from 'react'; class Wrap extends React.Component { render() { return
{this.props.children}
; } } export default Wrap ``` 接下来编写测试模板: ``` jest .dontMock('../source/components/Actions') .dontMock('./Wrap') ; import React from 'react'; import TestUtils from 'react-addons-test-utils'; const Actions = require('../source/components/Actions'); const Wrap = require('./Wrap'); describe('Click some actions', () => { it('calls you back', () => { /* 渲染 */ const actions = TestUtils.renderIntoDocument(
); /* 搜索并检查节点 */ }); }); ``` ### 2. mock 函数 `
`组件并没有什么特别的地方。我们回忆一下,其代码如下: ``` const Actions = (props: Props) =>
ℹ
{/* 另外两个span标签 */}
``` 唯一需要测试的功能是,当点击这些按钮时,能否正确地调用onAction 回调函数。Jest 允许你定义mock 函数,并验证函数如何被调用。这用于验证我们的回调函数再合适不过了。 在测试代码中,你需要创建一个新的mock 函数,并将其以回调函数的形式传递给Actions: ``` const callback = jest.genMockFunction(); const actions = TestUtils.renderIntoDocument(
); ``` 接下来模拟点击动作按钮: ``` TestUtils .scryRenderedDOMComponentsWithTag(actions, 'span') .forEach(span => TestUtils.Simulate.click(span)); ``` 注意我们使用了TestUtils 中的一个方法寻找DOM 节点。该方法返回一个包含三个`
`节点的数组,然后你逐一点击数组中的每个节点。 现在你的mock 回调函数必然会被调用三次。你需要在expect() 方法中对此进行断言: ``` const calls = callback.mock.calls; expect(calls.length).toEqual(3); ``` 如你所见,callback.mock.calls 属性是一个数组。每当函数被调用时,都会传递一个包含参数的数组到该属性中。 第一个动作按钮的名称是info,在回调时通过props.onAction.bind(null, 'info') 的形式把类型info 传递到回调函数中。因此,第一次调用mock 回调(0)的第一个参数(0)必然是info: ``` expect(calls[0][0]).toEqual('info'); ``` 另外两个按钮的断言也类似: ``` expect(calls[1][0]).toEqual('edit'); expect(calls[2][0]).toEqual('delete'); ``` ### 3. find 与scry TestUtils(https://facebook.github.io/react/docs/test-utils.html )提供了一系列函数,帮助你在React 渲染树中寻找DOM 节点。比如,根据标签名或者类名寻找节点。前面的例子用到了其中一种方法: ``` TestUtils.scryRenderedDOMComponentsWithTag(actions, 'span') ``` 另一种方法是: ``` TestUtils.scryRenderedDOMComponentsWithClass(actions, 'ActionsInfo') ``` 与scry* 方法相对应的是find* 方法。比如: ``` TestUtils.findRenderedDOMComponentWithClass(actions, 'ActionsInfo') ``` 注意上述方法中Component 和Components 的区别。scry* 系列方法找出所有匹配的节点,并返回一个数组(甚至在只匹配一个或者零个节点时也是如此),而find* 系列方法只返回一个节点。如果没有匹配节点或者匹配了多个节点时,后者就会报错。因此,当你使用find* 系列方法进行寻找时,已经意味着假定在DOM 树中只存在一个DOM 节点。 # 6、更多模拟交互 接下来测试Rating 组件。在鼠标移入、移出和点击时,其状态会发生改变。测试的模板代码如下: ``` jest .dontMock('../source/components/Rating') .dontMock('classnames') ; import React from 'react'; import TestUtils from 'react-addons-test-utils'; const Rating = require('../source/components/Rating'); describe('works', () => { it('handles user actions', () => { const input = TestUtils.renderIntoDocument(
); /* 在此编写expect()的期望结果 */ }); }); ``` 注意这里不需要在渲染时包裹`
` 组件,因为这个组件不是无状态函数式组件。组件中有很多星星(默认为5 个),每个span 标签对应一颗星星。我们可以通过以下方式获取它们: ``` const stars = TestUtils.scryRenderedDOMComponentsWithTag(input, 'span'); ``` 现在测试需要模拟鼠标移入和移出事件,并点击第4 颗星星(span[3])。这时,前面4 颗星星都应该被“点亮”,也就是包含RatingOn 类名,而第5 颗星星应该维持原有的“熄灭”状态: ``` TestUtils.Simulate.mouseOver(stars[3]); expect(stars[0].className).toBe('RatingOn'); expect(stars[3].className).toBe('RatingOn'); expect(stars[4].className).toBeFalsy(); expect(input.state.rating).toBe(0); expect(input.state.tmpRating).toBe(4); TestUtils.Simulate.mouseOut(stars[3]); expect(stars[0].className).toBeFalsy(); expect(stars[3].className).toBeFalsy(); expect(stars[4].className).toBeFalsy(); expect(input.state.rating).toBe(0); expect(input.state.tmpRating).toBe(0); TestUtils.Simulate.click(stars[3]); expect(input.getValue()).toBe(4); expect(stars[0].className).toBe('RatingOn'); expect(stars[3].className).toBe('RatingOn'); expect(stars[4].className).toBeFalsy(); expect(input.state.rating).toBe(4); expect(input.state.tmpRating).toBe(4); ``` 此外还要注意测试是如何获取组件状态的,以验证state.rating 和state.tmpRating 的正 确性。对于测试而言,这可能有点苛刻了。毕竟如果外部的显示结果符合预期,为何还要 关心组件内部的状态呢?此处只是为了证明这样做是可行的。 # 7、测试完整的交互 接下来为Excel 组件编写测试。毕竟这个组件是整个应用的关键,一旦该组件出现问题,就会严重影响应用体验。事不宜迟,开始编写测试: ``` jest.autoMockOff(); import React from 'react'; import TestUtils from 'react-addons-test-utils'; const Excel = require('../source/components/Excel'); const schema = require('../source/schema'); let data = [{}]; schema.forEach(item => data[0][item.id] = item.sample); describe('Editing data', () => { it('saves new data', () => { /* 渲染组件、模拟交互、检查节点 */ }); }); ``` 首先要注意到顶部的jest.autoMockOff(); 函数。这里没有逐一列出Excel 使用的所有组件(以及组件内使用的组件),你可以通过这个方法直接禁用所有的mock。 接下来,需要用schema 对象和示例数据data 对组件进行初始化工作(和app.js 类似)。 然后进行渲染: ``` const callback = jest.genMockFunction(); const table = TestUtils.renderIntoDocument(
); ``` 目前看起来一切正常。现在改变第一行的第一个单元格。设置新的值为: ``` const newname = '$2.99 chuck'; ``` 目标单元格为: ``` const cell = TestUtils.scryRenderedDOMComponentsWithTag(table, 'td')[0]; ``` 在本书编写时,需要编写一些额外的hack 代码以支持访问dataset 属性,因为Jest 尚未支持这种DOM 操作: ``` cell.dataset = { // 针对Jest 的DOM 兼容性问题采取的非常规手段 row: cell.getAttribute('data-row'), key: cell.getAttribute('data-key'), }; ``` 双击单元格,其内容会变为一个包含文本输入框的表单: ``` TestUtils.Simulate.doubleClick(cell); ``` 改变输入框的值,并提交表单: ``` cell.getElementsByTagName('input')[0].value = newname; TestUtils.Simulate.submit(cell.getElementsByTagName('form')[0]); ``` 现在单元格的内容不再是表单了,而是纯文本内容: ``` expect(cell.textContent).toBe(newname); ``` 此时onDataChange 回调函数会被调用。该函数接收一个数组参数,数组中包含了表格数据的键值对。你可以验证mock 回调函数是否正确接收到了新的数据: ``` expect(callback.mock.calls[0][0][0].name).toBe(newname); ``` 此处[0][0][0] 的含义为:第一个0 对应mock 函数第一次被调用;第二个0 对应函数接收的首个参数,它在这里是数组;第三个0 对应数组中的第一个元素,它在这里是一个对象(对应表格中的一条记录),其中的name 属性值为$2.99 chuck。 除了使用TestUtils.Simulate.submit 提交表单,你还可以选用TestUtils.Simulate.keyDown 模拟敲下回车键时的事件。这同样可以提交表单。 现在编写第二个测试用例,我们删除一行示例数据: ``` it('deletes data', () => { // 和之前相同 const callback = jest.genMockFunction(); const table = TestUtils.renderIntoDocument(
); TestUtils.Simulate.click( // 点击图标 TestUtils.findRenderedDOMComponentWithClass(table, 'ActionsDelete') ); TestUtils.Simulate.click( // 确认对话框 TestUtils.findRenderedDOMComponentWithClass(table, 'Button') ); expect(callback.mock.calls[0][0].length).toBe(0); }); ``` 在前面的例子中,callback.mock.calls[0][0] 对应用户交互后产生的新数据。但这个例子 中的数组是空的,因为在测试中删除了单行记录。 # 8、代码覆盖率 掌握了上述这些主题后,剩下的事情就简单了,但可能会有一点枯燥。你需要确保尽可能多地测试所有可能发生的场景。比如点击info 行为按钮并点击取消,点击删除并点击取消,再次点击并删除。 测试是很有必要的,可以帮助你更快地解决问题、更自信地进行开发与代码重构。测试还可以帮助你纠正同事的错误:当他们觉得改变某一处地方不会造成什么影响时,事实可能远远超出他们的预想。一种“游戏化”测试过程的方式是使用代码覆盖率(code coverage)特性。 运行: ``` $ jest --coverage ``` 该命令会运行找到的所有测试并生成一份报告,告诉你已经测试(或覆盖)了多少行代码、多少个函数等。示例结果如图7-2 所示。  图7-2:代码覆盖率报告 你会发现目前的测试报告并非完美,必然要编写更多的测试用例。代码覆盖率报告的一个实用特性是显示未被覆盖的行号。比如,尽管你已经对FormInput 进行了测试,但第22 行还没有被覆盖。我们找出这行代码,发现它是一个return 语句: ``` getValue(): FormInputFieldValue { return 'value' in this.refs.input ? this.refs.input.value : this.refs.input.getValue(); } ``` 看来我们还没有测试过这个函数。因此赶紧编写一个测试用例作为补救措施: ``` it('returns input value', () => { let input = TestUtils.renderIntoDocument(
); expect(input.getValue()).toBe(String(new Date().getFullYear())); input = TestUtils.renderIntoDocument(
); expect(input.getValue()).toBe(3); }); ``` 第一个expect() 测试了一个内建的DOM 输入元素,而第二个expect() 则测试了自定义的input 组件。现在getValue() 方法中的三元表达式对应的两种情况应该都会被覆盖了。 再次运行上述命令。在目前的代码覆盖率报告中,关于第22 行的提示已经消失了(如图7-3 所示)。  图7-3:修改后的代码覆盖率报告