下面的文章会默认读者了解 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元素以及windowdocument对象来进行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);})
});
复制代码

beforeEachafterEach两个hook在每一个case执行之前或者之后,会分别执行,在每个case之前,进行global.Date.now的改写,然后在case结束之后,将global.Date.now恢复为原本的方法。

jest.fn会生成一个Mock函数,这个函数和其他函数不一样的地方在于,这个函数会记录到其被执行的一些信息,比如:

  • 函数被执行的次数
  • 函数每次被执行时的参数
  • 甚至是函数每次被调用时的this指向

可以看到,对于所有的Date.now()方法,得到的当前时间都被复写成了一个确定的数字,这样就可以保证你的测试用例的时间无关性。

对于historyDate.now这类挂载到window或者document上面的实例对象,我们都可以通过jest.fn来复写其方法,保证这些方法被调用的顺序以及调用结果的正确性,我们也可以在jest.fn内部进行断言,从而判断每次执行的过程中是否发生错误。

fetch请求

前端作为View,部分场景下比较依赖后端提供的Model来进行渲染,API的正确性很多时候会直接影响到整个页面的渲染结果是否正确。

并且部分场景中,某些代码也许是在Promiseresolve了之后才会被调用。

所以我们需要模拟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到服务端,在得到了正确的回调之后,清空掉输入框中的内容。

这种需求比较普遍,现在需要为这样一个需求添加一组单元测试,保证这个组件能够稳定运行。

考虑到几个重点:

  1. 触发输入框onchange事件
  2. 等待输入框输入事件结束
  3. 触发按钮点击事件
  4. 进行fetch
  5. 等待fetch结束
  6. 回调中清理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单元测试相关推荐

  1. 全网最细:Jest+Enzyme测试React组件(包含交互、DOM、样式测试)

    介绍 Jest是目前前端工程化下单元测试火热的技术栈,而Enzyme的支持提供了Jest测试React业务.组件的能力,下面来介绍一下React组件测试的一些实际场景. 1. 测试依赖包 " ...

  2. Jest+Enzyme测试React组件(上)

    React函数式组件 fb团队推荐使用函数式组件进行开发,但是函数是无状态的, 用class组件不香嘛,自带state状态,为什么要换写法?原因我们就来讲讲. 1. hooks是比HOC和render ...

  3. React单元测试:Jest+Enzyme

    一.概述 本文介绍基于Jest+Enzyme的React单元测试编写方法,包括对组件.action.reducer和其他工具类和功能类js的测试.主要介绍对组件.action.reducer代码的单元 ...

  4. react 单元测试 (jest+enzyme)

    react 单元测试 (jest+enzyme) 为什么要做单元测试 作为一个前端工程师,我是很想去谢单元测试的,因为每天的需求很多,还要去编写测试代码,感觉时间都不够用了. 不过最近开发了一个比较复 ...

  5. 使用Jest进行React单元测试

    React单元测试方案 前置知识 为什么要进行测试 测试可以确保得到预期的结果 作为现有代码行为的描述 促使开发者写可测试的代码,一般可测试的代码可读性也会高一点 如果依赖的组件有修改,受影响的组件能 ...

  6. JavaScript 测试系列实战(一):使用 Jest 和 Enzyme 测试 React 组件

    你或许早已经知道"单元测试""端到端测试"这些名词,但从未真正付诸实践.在这一系列实战教程中,我们将手把手带你掌握 Jest.Enzyme.Cypress 等测 ...

  7. React Jest + enzyme 配置 及 简单用例

    这里简单的介绍一下 React 项目下 Jest + enzyme 配置 并运行一个简单的测试用例. 这里跳过React项目的创建,React项目创建可以看下 React项目创建 1.安装 jest ...

  8. Jest + Enzyme React 组件测试实践

    ≈ 最近把组件测试接入到日常开发,提高了项目代码健壮性,可维护性.本人也从0到1收获了组件测试的经验. 本文总结一下最近两周 组件测试 相关的研究,包括: Jest + Enzyme 的基本介绍 Je ...

  9. 搭建 Jest+ Enzyme 测试环境

    1.为什么要使用单元测试工具? 因为代码之间的相互调用关系,又希望测试过程单元相互独立,又能正常运行,这就需要我们对被测函数的依赖函数和环境进行mock,在测试数据输入.测试执行和测试结果检查方面存在 ...

最新文章

  1. 统计学原理-----概率分布
  2. ubuntu14.04 qt4 C++开发环境搭建
  3. 双目深度估计中的自监督学习概览
  4. SQL基础【十一、分页 limit top rownum】
  5. JAVA实现在面板中添加图表_Java 创建PowerPoint图表并为其添加趋势线
  6. 丁香园 武汉 神童_扒一扒武汉同济、协和规培待遇
  7. Windows 2000缓冲区溢出技术原理
  8. php秒杀框架,yii框架redis结合php实现秒杀效果(实例代码)
  9. 纠错编码--海明码(动一发而牵全身)
  10. VS2019 windows桌面应用_桌面美化神器RocketDock EX增强版整合超多皮肤/图标哦!
  11. plt文件怎么转化为txt文件
  12. php矢量瓦片,【教你一招】张海平:如何将小范围在线地图切片数据转换为GIS矢量数据?...
  13. 电脑退域后登陆不上_退域后加域不成功问题
  14. -----已搬运-------Linux的/proc/self/学习 ++ CTF例题
  15. C#基于虹软SDK人脸识别签到系统
  16. SUN公司Java认证和考试纵览
  17. 未选择的路*弗罗斯特
  18. 访问网站浏览器左上角提示:windows 没有足够信息,不能验证该证书
  19. Google 的封杀与被封杀
  20. 《Adobe After Effects CC完全剖析》——时间设置

热门文章

  1. 西达摩花魁咖啡豆名字来源
  2. 视频教程-图解Python编程神器Jupyter Notebook-Python
  3. 秘猿科技开源 CITA-Monitor
  4. 投资组合管理-风险分散与马科维茨均值方差模型
  5. 经济专业需要学c语言吗,学c语言要什么基础?
  6. 【大家说英语】Work Rob Gives a Speech
  7. 3D房地产营销PPT模板
  8. 收集N个超实用的 JS 片段( ES6+ 编写),你和大神只差这个宝典
  9. 微商最低成本引流,学会这招日引精准粉1000+
  10. 开源并“免费”的Linux平台DAW——Ardour 4.0发布