最近公司的一个前端 react 项目准备做单元测试,因为是老项目,之前已经用了一套 Mocha + enzyme + assert + Sinon + Istanbul 进行单元测试,不过上一次单元测试已经比较久远,加上我本人对单元测试并不了解,所以我找了各种文章,系统地学习了一下前端的单元测试,对比评估几个主流前端测试框架,为项目这次的单测提供一些参考意见,顺道在这里分享下我的学习成果。
一、定义及目的
-
定义:主要是把代码看成一个个的函数以及组件,通过编写测试脚本(测试用例),以最小的颗粒度去测试,确保这些函数和组件都能达到预期值。
-
目的:提高代码质量和可维护性,防止开发的后期因 bug 过多而失控。(更多不是体现在新代码的编写上,而是对已有代码的更改。)
在单元测试的过程中,我们可能需要编写并运行尽可能多的单测用例,来确保能够发现更多 Bug,特别是当需要修改或者重构代码的时候,有一组可靠的单元测试做保护可以让我们的操作更安全,更有信息不会对系统的未知破坏。
二、框架种类
目前,前端比较流行的框架有三个:Jest、Mocha、Jasmine。
下图是近一年这三个框架的下载量。
通过对比可以看到,Jest 下载量遥遥领先!不过从下载量来看,Mocha 和 Jasmine 也是有很大一批开发人员在使用的。
下面说说这三个框架的优缺点。
| 框架 | 断言库 | Mock | 覆盖率 |
|---|---|---|---|
| Jest | Expect | 支持 | Istanbul |
| Mocha | 无 | 否 | 无 |
| Jasmine | 有 | 支持 | 无 |
- Jest:框架功能齐全,内置断言库Expect、内置 mock 模块,自带 snapshot 功能,同时内置 Istanbul 查看覆盖率,不需要开发者额外配置,对React组件支持度非常友好。
- Mocha:属于基础框架,不内置断言库、mock 等,如果需要则需要添加其他库/插件完成,需要开发者自行去选择断言库。
- Jasmine:安装即用,支持断言、仿真、全局环境,稳定性较好,文档完整。
ps:断言:判断源码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误。
Mock:一般指提供监听函数调用等功能的库
在了解了相关框架以后,可能有时候光有框架还不够,需要自己额外添加库或辅助工具,下面看看有哪些库吧~
三、库种类
- 断言库:chai、assert、expect、should
- 渲染库:enzyme
- 请求工具:nock
- 模拟工具:JSDOM
- 覆盖率:Istanbul
可以看出断言库种类很多,下面贴一张这几个框架近一年的下载量,给各位做参考
ps: should.js has been archived by the owner.
四、框架的使用
前面已经简单介绍了单元测试的工具,如何安装配置这里不做赘述,让我们来看看代码上应该怎么写吧~
首先让我们了解一下测试框架的一些api,这里我们先用 Mocha + Chai 举例。
比如我们对Sum函数写用例。
function sum(a, b) {
return a + b;
}
Mocha + Chai 方式:
const {expect, assert} = require('chai');
const sum = require('./sum');
describe('sum function test', () => {
// BDD 方式
it('should be 3 using expect', () => {
expect(sum(1, 2)).to.equal(3);
});
// TDD 方式
it('should be 3 using assert', () => {
assert.equal(sum(1, 2), 3);
});
});
这里Mocha框架提供了2个api:describe、it。
describe:作用是“测试套件”,表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称,第二个参数是一个实际执行的函数。
it:作用是“测试用例”,表示一个单独的测试,是测试的最小单位,它也是一个函数,第一个参数是测试用例的名称,第二个参数是一个实际执行的函数。
我们从 Chai 里引入了断言函数expect/assert,用于进行断言判断。
这一段代码在 Jest 里需要这样写:
const sum = require('./sum');
describe('sum function test', () => {
// 官方默认写法
test('should be 3', () =>
expect(sum(1, 2)).toBe(3);
});
// it 等同于 test
it('should be 3', () => {
expect(sum(1, 2)).toBe(3);
});
})
ps:由于 Jest 内置了断言库 expect,所以无需引入其他断言库。
对比两个框架,断言库 Chai 支持 BDD 和 TDD 的写法 assert/expect,而 Jest 内置的断言库 expect 只支持 BDD 写法。
assert 断言有很多常用api,在这里介绍下两个常用的(下面例子使用的库为 assert)
- assert( value [, message] )
功能:判断值是否为真值
第一个参数为要判断的值,第二个是可选参数,为错误时抛出的错误信息
const assert = require('assert')
assert( '1', '第一个值为false时,错误信息抛出')
assert( true, '第一个值为false时,错误信息抛出')
// 测试通过
assert( false, '第一个值为false时,错误信息抛出')
// AssertionError [ERR_ASSERTION]: 第一个值为false时,错误信息抛出
- assert.strictEqual( actual, expected, [, message] )
功能:判断预期值和实际值全等(===)
第一个参数为要判断的值,第二个参数为期望值,第三个是可选参数,为错误时抛出的错误信息
const assert = require('assert')
assert.strictEqual( 1, 1 );
// 测试通过
assert.strictEqual( 1, '1' );
// AssertionError [ERR_ASSERTION]: 1 === '1'
当然断言库还有很多好用的常用的api,在这里不做赘述,放上各个常用断言库的地址给各位~
expect: Jest GitHub
assert: assert GitHub
chai: chai 官网
ps:TDD 和 BDD 详解(两种测试方法)
1. TDD (Test-Driven Development) 测试驱动开发
TDD 的一般过程是
1)写一个测试
2)运行这个测试,看到预期的失败
3)编写尽可能少的业务代码,让测试通过
4)重构代码
5)不断重复以上过程
- 优点:可维护性极高,你对代码的任何修改、扩展、重构都能得到及时的反馈,不用担心会无意中破坏系统。
- 缺点:学习 TDD 的最大障碍在于需要先写测试代码,然后才是产品代码,这是个思维转换和习惯养成的过程,需要不断的重复练习才能逐步掌握。
2. BDD (Behavior-Driven Development) 行为驱动开发
BDD是一组编写优秀自动化测试的最佳实践,可以单独使用,但是更多情况下是与 TDD 单元测试配合使用的。
BDD解决的一个关键问题就是如何定义TDD或单元测试过程中的细节。一些不良的单元测试的一个常见问题是过于依赖被测试功能的实现逻辑。这通常意味着如果你要修改实现逻辑,即使输入输出没有变,通常也需要去更新测试代码。这就造成了一个问题,让开发人员对测试代码的维护感觉乏味和厌烦。
BDD建议针对行为进行测试,我们不考虑如何实现代码,取而代之的是我们花时间考虑场景是什么,会有什么行为,针对行为代码应该有什么反应。
结论
单元测试回答的是What的问题,TDD 回答的是 When 的问题,BDD 回答的是 How 的问题。也可以把 BDD 看作是在需求与 TDD 之间架起一座桥梁,它将需求进一步场景化,更具体的描述系统应该满足哪些行为和场景,让 TDD 的输入更优雅、更可靠。你可以选择单独使用其中一种方法,也可以综合使用这几个方法以取得更好的效果。
渲染库 Enzyme
很多时候我们单元测试不仅仅是对javascript,我们使用了 React 这样的框架开发项目,这时我们单测的对象经常是以组件为单位,这时我们可以使用 Enzyme 来进行组件的渲染和测试,Enzyme 可以直接在 node 环境渲染虚拟 DOM ,并对其进行测试。
Enzyme 提供了三种级别的渲染方式:
- Shallow Rendering:shallow
浅渲染:只会渲染自己的部门,不会递归渲染子组件,渲染为虚拟DOM,所以速度很快。
例:
import { shallow, ShallowWrapper } from 'enzyme';
let wrapper: ShallowWrapper;
const props = {
// ...
}
wrapper = shallow(<ButtonGroup {...props} />);
- Static Rendering: render
静态渲染:像真实的运行环境一样,渲染的所有内容都会展示,采用的是第三方库Cheerio的渲染,用于生成 HTML,分析结构,对于snapshot 使用 render 比较合适。 (不需要 JSDOM 模拟环境解决子组件测试)
例:
import { render } from 'enzyme';
const wrapper = render(<ButtonGroup />);
- Full Rendering: mount
完整渲染:把组件包含子组件完全渲染。讲 React 组件挂载到真实 DOM 节点,可选择 JSDOM 库提供的浏览器环境的 Node.js 模拟 或 document.createElement()去创建真实节点。
例:
import { mount } from 'enzyme';
const wrapper = mount(<ButtonGroup />);
wrapper.find('button');
// 用完即卸载,防止内存泄露
wrapper.unmount();
Enzyme 还提供了一些非常实用的 api,下面简单介绍下~
- simulate( event, mock):用来模拟事件触发,event为事件名称,mock为一个event object
- instance():返回测试组件的实例
- find(selector):根据选择器查找节点,selector可以是CSS中的选择器,也可以是组件的构造函数,以及组件的display name等
- props():返回根组件的所有属性;
- prop(key):返回根组件的指定属性;
- state():返回根组件的状态;
- setState(nextState):设置根组件的状态;
- setProps(nextProps):设置根组件的属性;
ps:关于交互测试:
主要利用simulate()接口模拟事件,实际上simulate是通过触发事件绑定函数,来模拟事件的触发。触发事件后,去判断props上特定函数是否被调用,传参是否正确;组件状态是否发生预料之中的修改;某个dom节点是否存在是否符合期望。
Enzyme 如何编写测试用例?
-
使用find(‘.className')按类名查找,并使用text()获取文本。
-
使用find(‘button')按标签名称查找,并使用at(0)获取第一个button。使用simulate('click')模拟点击事件。
-
模拟多次点击
-
使用state()函数获取组件state,并进行验证
-
使用find(‘#list')通过id进行查找,并验证dom结构的修改
-
使用props()和prop(key)函数来获取组件的props,并可通过setProps改变组件props验证组件是否正确根据props进行变化。
如果使用了redux,store和redux state也是通过connect,以props的形式传递给组件,同样可以进行测试。
- 获取组件下面结构的类名,验证各种操作下类名是否正确改变。
辅助库 Sinon
Sinon 主要功能是通过伪装和拦截,来模拟与其他系统或函数的操作,解耦测试的依赖。
提供了三个api:
- Spy: 可以提供函数调用的信息,但不会改变函数的行为,它会记录下函数调用的参数、返回值、this的值以及抛出的异常。
- Stub: 提供函数的调用信息,并且可以像示例代码中一样,让被 stubbed 的函数返回任何我们需要的行为。stub 也能匿名,也能去封装和监听已有函数,但当 stub 封装了一个已有函数后,原函数不会再被调用。
- Mock: 通过组合 spies 和 stubs,使替换一个完整对象更容易,和 stub 很像,stub 是对对象中单个函数的监听和拦截,而。mock 是对多个。
尾言
以上是我个人看了一些单元测试文章后进行的一个汇总,许多内容都直接CV了别人文章原文,主要介绍前端单元测试是什么,具体对于 React 这样的框架如何进行单元测试没有详细讲,(之后可能会单独发一篇讲讲),希望认真看完的你有问题可以评论区留言或者私信给予建议~
生命不息,学习不止!
最后放上借鉴或者看到的文章链接: