Jest enzyme 进行react单元测试
下面的文章会默认读者了解 React及其技术栈以及基本的前端单元测试,由于本文涉及到的技术较多,故不宜一一在文中介绍,谅解。
写在前面
在撰写单元测试用例之前,我们需要了解到撰写测试用例的原因。写测试用例的目的在于保证代码的迭代安全,并不是为了100%的coverage或者是case pass,coverage和case仅仅是为了实现代码安全的因素。
单元测试(Unit Test):前端单元测试,在以前也许是一个比较陌生的工作,但是前端在经历了这几年的发展之后,我们对于代码的鲁棒性要求逐渐提升,承载了更多的业务逻辑的同时,作为整个链路上最接近用户的部分,系统崩溃阻塞的成本非常之高。如果你采用的是SSR,那么直接在服务端渲染报错则是更为致命的。
前端的单元测试能够在一定程度上保证:
- 在迭代过程中保证每次提交的代码的质量;
- 在代码的重构过程中,原始功能的完整性;
- 每次代码迭代的副作用可控;
相对于后端代码来说,前端代码更多地会涉及到DOM相关的内容,对于非结构化的内容如何进行测试呢?
airbnb提供了一个比较合适的React单元测试解决方案,结合Jest以及husky,可以保证每次commit的代码都符合规范,并且coverage内的代码功能完整。
UT之于library
库对于单元测试的要求是非常高的。因为一个lib可能被多个业务线以及工程所引入,一旦这个lib出现了任何问题,影响到的范围是非常大的。我们又不可能要求QA对于多个业务线进行回归(怕是他们要杀了我们祭天吧)。
为了保证lib的迭代不会影响到原有的业务功能,单元测试是一个非常好的方法。由于我们主要的技术栈还是基于React的各种解决方案,所以有比较多的业务组件以及公共组件,这些组件被多个业务线使用。lerna架构的组件工程在每次commit的时候都会跑UT,来进行功能回归。
UT之于业务
业务代码一般对于单元测试的需求并不如lib那样高,但是在某些核心业务逻辑中接入UT,也是可以保证代码整体的质量的。最起码可以保证业务代码在正常的渲染过程中不发生报错。
框架
前面简单描述了一下单元测试对于前端代码的重要性,很多人说现在的前端圈子和娱乐圈一样,确实,目前可选的测试框架林林总总有很多,经历了jasmine、mocha,现在来到了Jest。
TL;DR
9102年了,Jest可以说是目前前端最好的测试框架了。可以进行快速配置,和enzyme很好地结合,能够保证在React技术栈中,快速跑起来一个测试用例。
但是,最吸引人的还是其内置的coverage报告,可以快速生成代码覆盖率。
相比于测试框架,React的测试库似乎没有什么其他的选择了,enzyme基本可以满足任何前端的测试需求。但是对于异步强交互的页面来说,撰写测试用例的学习成本还是比较高的。
技术栈
最终我们为了各种场景下React的单元测试,集成了下面的lib:
- Jest:单元测试框架
- enzyme: React测试库
- Nock: 异步请求模拟
- Async-wait-until: 异步操作结束通知
- Husky: pre-commit阶段执行单元测试
配置
Jest
Jest本身就以配置简单著称,而enzyme更是可以即插即用的测试库。所以配置过程要比较轻松。
module.exports = {// 单元测试环境根目录rootDir: path.resolve(__dirname),// 指定需要进行单元测试的文件匹配规则testMatch: ['<rootDir>/test/**/__test__/*.js'],// 需要忽略的文件匹配规则testPathIgnorePatterns: ['/node/modules'],testURL: 'http://localhost/',// 是否收集测试覆盖率,以及覆盖率文件路径collectCoverage: true,coverageDirectory: './coverage'
};
复制代码
上面是几个比较重要的配置项。其中大部分都是比较好理解的,而testURL
这个配置项需要说明一下,这个规则表示当前测试用例所运行的URL,虽然测试的时候我们看不到完整的页面,但是测试用例本身是挂载到一个页面中的,而这个页面的URL就是通过testURL
指定的。
在这个Jest配置下,所有的测试用例中,如果执行location.href
都会拿到http://localhost/
这个URL的,这个配置项在进行需要网络请求的case中是很关键的。
在执行的时候,可以指定Jest的配置文件路径:
~ jest --config ./scripts/jest.config.js
复制代码
如果没有指定文件路径的话,默认则是取当前文件路径的配置文件。
enzyme
enzyme本身是不需要配置的,作为一个即插即用的React测试库,也算是让我们前端脱离了配置工程师的苦海。
但是基于React进行开发,则需要安装对应的React Adapter,比如如果你需要使用static getDerivedStateFromProps
方法,那么就需要引入enzyme-adapter-react-16
的库来保证enzyme渲染的版本和你使用的版本是一致的。
Jest在进行UT的过程中,会首先检查工程是否有配置.babelrc
文件,如果配置了,则会自动根据这个文件来进行babel编辑,然后执行测试用例。
一个随手搭建的演示环境的依赖:
"dependencies": {"react": "^16.7.0","react-dom": "^16.7.0"},"devDependencies": {"babel-plugin-transform-async-to-generator": "^6.24.1","babel-plugin-transform-class-properties": "^6.24.1","babel-preset-env": "^1.7.0","babel-preset-es2015": "^6.24.1","babel-preset-react": "^6.24.1","babel-preset-stage-0": "^6.24.1","babel-preset-stage-3": "^6.24.1","enzyme-adapter-react-16": "^1.7.1","enzyme": "^3.8.0","jest": "^23.6.0"},"scripts": {"test": "jest --config ./jest.config.js"}
复制代码
// ./__test__/index.js
import Test from '../src';
import Enzyme, { shallow, render, mount } from 'enzyme';
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';Enzyme.configure({adapter: new Adapter()
});
复制代码
而enzyme的adapter是需要进行初始化的,通过Enzyme.configure
指定需要引入的adapter实例。
这样就完成了一个Enzyme + React + Jest的环境。
撰写一个简单的测试用例
断言
目前,各种测试框架的断言已经开始收敛,Jest采用的断言语法和我们之前使用的mocha语法类似。
一个test suite可以用describe
来描述,一个test suite可以包含多个case,来测试各种场景下的组件渲染结果。
我们先给出一个非常简单的React组件:
import React from 'react';export default class Text extends React.Component {render() {return (<div className="test-container" />)};
}
复制代码
对于这个组件,我们需要判断是否成功渲染出来了div元素,并且元素的类名是test-container
。
这是一个极简版本的case:
describe('test suite: Test component', () => {it('case: expect Test render a div with className: test-container', () => {const wrapper = shallow(<Test />);expect(wrapper.find('.test-container').length).toEqual(1);});
});
复制代码
执行npm run test
,可以得到下面的结果:
可以看到suites和cases的通过情况,以及各种覆盖率结果。其实前端单元测试也可以这么简单的。
关于enzyme的三个核心渲染方法,mount、render以及shallow,网上有很多文章介绍三者之间的区别,这里就不班门弄斧了。mount应该是我写测试用例最常用的方法吧,毕竟大部分组件的逻辑都需要真实挂载出来,才能够进行用例测试。
测试用例也可以很复杂
最近有一个比较复杂的组件,需要接入单元测试,当时在开发的时候太天真,现在想起来真的是追悔莫及。组件内部包含:fetch请求、时间获取、history
操作,并且含有非常多的人机交互逻辑。
这样的组件现在想起来是非常不规范的,但是为了保证以后修改的时候,业务逻辑的鲁棒,也不得不强行为其添加单元测试。
下面有很多case,大部分case都是在实际coding过程中遇到的,希望能够帮助到有同样需求的人。
history和Date.now()
在业务代码中,很多时候我们都需要进行页面的跳转,或者hash的修改。所有对于location
的操作都会落在window.location
的对象上。
enzyme实际上为我们构建了一个虚拟的DOM环境,我们可以拿到对应的DOM元素以及window
、document
对象来进行DOM操作。
Date
也是类似的,也是一个全局的对象,以前我们通过集成js-dom
来进行模拟,而现在enzyme和Jest为我们做好了这些工作。
看下面这个组件:
class Time extends React.Component {static propTypes = {time: PropTypes.number};constructor(props) {super(props);this.state = {before: Date.now() < props.time}}render() {const { before } = this.state;const { time } = this.props;if (before) {return (<div className="before">{`now is before time: ${time}`}</div>);} else {return (<div className="after">{`now is after time: ${time}`}</div>);}}
}复制代码
在撰写单元测试的时候,我们会发现,由于当前时间的不一致,所以作为props
传入的时间在和Date.now()
进行比较,得到的结果是不一致的,这样会导致测试用例的结果不可控。
为了保证Date.now()
得到的值是一致的,我们需要改写DOM上的Date
对象。
describe('test suite: Time component', () => {const NOW_TO_CACHE = global.Date.now;const NOW_TO_USE = jest.fn(() => 1547717952668);beforeEach(() => {global.Date.now = NOW_TO_USE;});afterEach(() => {global.Date.now = NOW_TO_CACHE;});it('case: now is less than props\' time', () => {const wrapper = shallow(<Time time={1547717952669} />);console.log(Date.now())expect(wrapper.find('.before').length).toEqual(1);});it('case: now is greater than props\' time', () => {const wrapper = shallow(<Time time={1547717952667} />);console.log(Date.now())expect(wrapper.find('.after').length).toEqual(1);})
});
复制代码
beforeEach
和afterEach
两个hook在每一个case执行之前或者之后,会分别执行,在每个case之前,进行global.Date.now
的改写,然后在case结束之后,将global.Date.now
恢复为原本的方法。
jest.fn
会生成一个Mock函数,这个函数和其他函数不一样的地方在于,这个函数会记录到其被执行的一些信息,比如:
- 函数被执行的次数
- 函数每次被执行时的参数
- 甚至是函数每次被调用时的
this
指向
可以看到,对于所有的Date.now()
方法,得到的当前时间都被复写成了一个确定的数字,这样就可以保证你的测试用例的时间无关性。
对于history
、Date.now
这类挂载到window
或者document
上面的实例对象,我们都可以通过jest.fn
来复写其方法,保证这些方法被调用的顺序以及调用结果的正确性,我们也可以在jest.fn
内部进行断言,从而判断每次执行的过程中是否发生错误。
fetch请求
前端作为View,部分场景下比较依赖后端提供的Model来进行渲染,API的正确性很多时候会直接影响到整个页面的渲染结果是否正确。
并且部分场景中,某些代码也许是在Promise
被resolve
了之后才会被调用。
所以我们需要模拟fetch请求,来保证在请求回调中的代码被单元测试覆盖到。
这里就需要用到:
Nock:HTTP server mocking and expectations library for Node.js
Async-wait-until:Wait while predicate completes and resolve a Promise
这两个库了。
首先,看下面这个组件:
import React from 'react';
import fetch from 'isomorphic-fetch';export default class AsyncComponent extends React.Component {constructor(props) {super(props);this.state = {user: {}}}componentDidMount() {this.fetchUser().then(res => {this.setState({user: res});});}fetchUser = () => {return fetch(`${location.origin}/api/user/get`, {method: 'GET'}).then(ret => {return ret.json();}).catch(err => {console.error(err);});}render() {const { user } = this.state;return (<div className="user-profile"><p className="name">{user.name}</p><p className="age">{user.age}</p></div>);}
}
复制代码
组件内部在componentDidMount
阶段进行了一次fetch请求,来在客户端渲染的时候获取数据,填充到页面中。
同步的测试工作非常简单,根据前面的几个例子,相信你可以对于渲染进行很好地测试了。
Q & A:
Q:其一:如何测试网络请求的回调呢?
我们不可能直接将UT的请求直接打到后台的接口里,这样在没有网络的环境下,UT是通过不了的。所以必须要在本地模拟到近似于真实的网络请求。
A:Nock
Q: 其二:网络请求时异步的,如果撰写异步的测试用例呢?
组件View的更新是在异步的请求resolve之后进行的,而测试用例的执行是同步的,这样就会出现时序问题,所以我们需要将断言和组件的fetch同步执行。
A: async-wait-until
这就是我们引入这两个库的原因了。具体如何结合这两个库来进行异步渲染的单元测试,看下面这个test suite。
import Async from '../src/async';
import Enzyme, { shallow, render, mount } from 'enzyme';
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import nock from 'nock';
import waitUntil from 'async-wait-until';Enzyme.configure({adapter: new Adapter()
});describe('test suite: Async component', () => {beforeAll(() => {nock('http://localhost/api/user').get('/get').reply(200, {"name": "lucas","age": 20});});afterAll(() => {nock.cleanAll();});it('case: expect component did mount will trigger re-render', async () => {const wrapper = mount(<Async />);await waitUntil(() => wrapper.state('user').name === 'lucas');expect(wrapper.find('.name').text()).toBe('lucas');expect(wrapper.find('.age').text()).toBe('20');});
});
复制代码
上面的这个测试用例的核心在于模拟fetch请求,并且等在请求结束再执行对应的断言。
首先,我们为这个test suite增加了两个hook,beforeAll
会在这个suite的所有case执行之前执行一次,而afterAll
则会在所有的case全部执行完之后,执行一次。
beforeAll
中,我们通过nock模拟了组件中fetch请求的请求结果,给到了一个resolve的响应。
当React执行到componentDidMount
的时候,会进行fetch请求,这个请求会被打到nock中。这里注意到,我们fetch的URL是http://localhost/api/user/get
,这就是之前提到的,Jest配置项中设置testURL
的作用。testURL
指定的URL会作为测试页面的location.origin
。
由于fetch是一个异步的过程,我们需要等待fetch被resolve之后,才能够进行断言。
所以,这里用到了waitUntil
,这个函数接受一个函数作为参数,这个函数会返回一个bool值,当bool值为true
的时候,表示异步调用结束,可以开始执行后面的逻辑了,当然,我们也可以封装一个自己的waitUntil
,其本质就是封装一个Promise。
结束了这一个suite之后,代码逻辑会走到afterAll
的hook中。这里面调用了nock.cleanAll()
,用于对之前mock的接口进行清理,也就是规范这个mock的作用域仅仅位于当前的suite中。
这时,我们再跑一次npm run test
,可以得到下面的测试结果:
结合上面的test suite,在单元测试中成功进行了fetch,并且渲染出了正确的结果。
但是细心的小伙伴可能会发现,coverage报告中有一行代码没有被这个test suite覆盖到,这行代码可以定位到fetch的reject中,因为我们仅仅测试了fetch resolve的情况。
为了测试reject的情况,我们需要一个新的suite,在这个suite中,我们mock一个reject响应的接口:
describe('test suite: Async component', () => {let resolve = false;beforeAll(() => {nock('http://localhost/api/user').get('/get').reply(400, () => {resolve = true;});});afterAll(() => {nock.cleanAll();});it('case: expect component fetch error will not block rendering', async () => {const wrapper = mount(<Async />);await waitUntil(() => resolve);expect(wrapper.find('.name').text()).toBe('');expect(wrapper.find('.age').text()).toBe('');});
});
复制代码
由于请求是异步的,并且与resolve的情况不同,我们不知道何时请求会被reject,所以我们需要给nock传入一个回调,来标识fetch结束,请求被reject。
这样就可以测试到reject情况下页面是否成功渲染了,保证了各种condition下,页面或者组件的稳定。
交互模拟
作为链路中toC的部分,前端代码中有许多地方是需要进行人机交互的。在交互过程中,javascript主要以注册事件的方式进行交互响应。
人机交互不仅仅是异步的,并且还包含事件的触发以及回调。这部分测试,enzyme提供了很多有意思的API,来帮助我们完成人机交互过程的单元测试。
考虑下面的这个组件:
import React from 'react';
import fetch from 'isomorphic-fetch';export default class Text extends React.Component {constructor(props) {super(props);this.state = {value: ''};}onInputChanged = (e) => {this.setState({value: e.target.value});}onClicked = () => {const { value } = this.state;this.postValue(value).then(res => {this.setState({value: ''});});}postValue = (value) => {return fetch(`${location.origin}/api/value`, {method: 'POST',body: JSON.stringify({value}),}).then(ret => {return ret.json();});}render() {const { value } = this.state;return (<div className="form"><input value={value} onChange={this.onInputChanged} /><button className="submit" onClick={this.onClicked}>提交</button></div>)}
}
复制代码
这是一个常见的React输入框,我们将输入框的value
绑定到state
上面。期望能够通过用户输入来改变组件状态,在用户点击提交的时候,可以从页面中取到这个值,并且POST到服务端,在得到了正确的回调之后,清空掉输入框中的内容。
这种需求比较普遍,现在需要为这样一个需求添加一组单元测试,保证这个组件能够稳定运行。
考虑到几个重点:
- 触发输入框onchange事件
- 等待输入框输入事件结束
- 触发按钮点击事件
- 进行fetch
- 等待fetch结束
- 回调中清理input内容
enzyme提供了一些触发事件的方法。当我们使用mount
将一个组件挂载到虚拟DOM上的时候,可以通过wrapper.simulate()
方法来触发各种DOM事件。
首先,先测试组件是否正确完成渲染:
it('case: expect input & click operation correct', async () => {const wrapper = mount(<Interaction />);const input = wrapper.find('input').at(0);const button = wrapper.find('button').at(0);expect(input.exists());expect(button.exists());
});
复制代码
然后需要触发input的onchange事件,来改变当前的state:
input.simulate('change', {target: {value: 'lucas'}
});expect(wrapper.state('value')).toBe('lucas');
复制代码
接着,触发按钮的点击事件,进行fetch请求,然后在响应返回之后,清理掉state
中的内容。
button.simulate('click');
复制代码
这样就完成了整个组件的操作流程的UT了,执行这个单元测试,可以发现我们的测试已经完全覆盖了所有代码的所有分支了。
下面是完成的test suite:
import Interaction from '../src/interaction';
import Enzyme, { shallow, render, mount } from 'enzyme';
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import nock from 'nock';
import waitUntil from 'async-wait-until';Enzyme.configure({adapter: new Adapter()
});describe('test suite: Async component', () => {let resolve = false;beforeAll(() => {nock('http://localhost/api').post('/value').reply(200, () => {resolve = true;return {};});});afterAll(() => {nock.cleanAll();});it('case: expect input & click operation correct', async () => {const wrapper = mount(<Interaction />);const input = wrapper.find('input').at(0);const button = wrapper.find('button').at(0);expect(input.exists());expect(button.exists());input.simulate('change', {target: {value: 'lucas'}});expect(wrapper.state('value')).toBe('lucas');button.simulate('click');await waitUntil(() => resolve);expect(wrapper.state('value')).toBe('')});
});
复制代码
整个测试用例完全pass,并且coverage为100%
最后
洋洋洒洒又是一个大长篇,有很多博主会将enzyme、nock、jest这类库分开来讲,但是在实际使用过程中,这几个库却是密不可分的。
单元测试是前端工程化的一个不可避免的阶段性工作,无论是开源工作还是业务工作,保证在每次迭代过程中代码的安全性于人于己都有很大的好处。
最后还是要说,撰写测试用例的时候,一定要切记,单元测试并不是堆砌覆盖率,而是保证每一个功能细节都被覆盖到,不要舍本逐末了。
Jest enzyme 进行react单元测试相关推荐
- 全网最细:Jest+Enzyme测试React组件(包含交互、DOM、样式测试)
介绍 Jest是目前前端工程化下单元测试火热的技术栈,而Enzyme的支持提供了Jest测试React业务.组件的能力,下面来介绍一下React组件测试的一些实际场景. 1. 测试依赖包 " ...
- Jest+Enzyme测试React组件(上)
React函数式组件 fb团队推荐使用函数式组件进行开发,但是函数是无状态的, 用class组件不香嘛,自带state状态,为什么要换写法?原因我们就来讲讲. 1. hooks是比HOC和render ...
- React单元测试:Jest+Enzyme
一.概述 本文介绍基于Jest+Enzyme的React单元测试编写方法,包括对组件.action.reducer和其他工具类和功能类js的测试.主要介绍对组件.action.reducer代码的单元 ...
- react 单元测试 (jest+enzyme)
react 单元测试 (jest+enzyme) 为什么要做单元测试 作为一个前端工程师,我是很想去谢单元测试的,因为每天的需求很多,还要去编写测试代码,感觉时间都不够用了. 不过最近开发了一个比较复 ...
- 使用Jest进行React单元测试
React单元测试方案 前置知识 为什么要进行测试 测试可以确保得到预期的结果 作为现有代码行为的描述 促使开发者写可测试的代码,一般可测试的代码可读性也会高一点 如果依赖的组件有修改,受影响的组件能 ...
- JavaScript 测试系列实战(一):使用 Jest 和 Enzyme 测试 React 组件
你或许早已经知道"单元测试""端到端测试"这些名词,但从未真正付诸实践.在这一系列实战教程中,我们将手把手带你掌握 Jest.Enzyme.Cypress 等测 ...
- React Jest + enzyme 配置 及 简单用例
这里简单的介绍一下 React 项目下 Jest + enzyme 配置 并运行一个简单的测试用例. 这里跳过React项目的创建,React项目创建可以看下 React项目创建 1.安装 jest ...
- Jest + Enzyme React 组件测试实践
≈ 最近把组件测试接入到日常开发,提高了项目代码健壮性,可维护性.本人也从0到1收获了组件测试的经验. 本文总结一下最近两周 组件测试 相关的研究,包括: Jest + Enzyme 的基本介绍 Je ...
- 搭建 Jest+ Enzyme 测试环境
1.为什么要使用单元测试工具? 因为代码之间的相互调用关系,又希望测试过程单元相互独立,又能正常运行,这就需要我们对被测函数的依赖函数和环境进行mock,在测试数据输入.测试执行和测试结果检查方面存在 ...
最新文章
- 统计学原理-----概率分布
- ubuntu14.04 qt4 C++开发环境搭建
- 双目深度估计中的自监督学习概览
- SQL基础【十一、分页 limit top rownum】
- JAVA实现在面板中添加图表_Java 创建PowerPoint图表并为其添加趋势线
- 丁香园 武汉 神童_扒一扒武汉同济、协和规培待遇
- Windows 2000缓冲区溢出技术原理
- php秒杀框架,yii框架redis结合php实现秒杀效果(实例代码)
- 纠错编码--海明码(动一发而牵全身)
- VS2019 windows桌面应用_桌面美化神器RocketDock EX增强版整合超多皮肤/图标哦!
- plt文件怎么转化为txt文件
- php矢量瓦片,【教你一招】张海平:如何将小范围在线地图切片数据转换为GIS矢量数据?...
- 电脑退域后登陆不上_退域后加域不成功问题
- -----已搬运-------Linux的/proc/self/学习 ++ CTF例题
- C#基于虹软SDK人脸识别签到系统
- SUN公司Java认证和考试纵览
- 未选择的路*弗罗斯特
- 访问网站浏览器左上角提示:windows 没有足够信息,不能验证该证书
- Google 的封杀与被封杀
- 《Adobe After Effects CC完全剖析》——时间设置
热门文章
- 西达摩花魁咖啡豆名字来源
- 视频教程-图解Python编程神器Jupyter Notebook-Python
- 秘猿科技开源 CITA-Monitor
- 投资组合管理-风险分散与马科维茨均值方差模型
- 经济专业需要学c语言吗,学c语言要什么基础?
- 【大家说英语】Work Rob Gives a Speech
- 3D房地产营销PPT模板
- 收集N个超实用的 JS 片段( ES6+ 编写),你和大神只差这个宝典
- 微商最低成本引流,学会这招日引精准粉1000+
- 开源并“免费”的Linux平台DAW——Ardour 4.0发布