近几年,前端发展越来越迅猛,各类框架层出不穷,前端实现的业务逻辑也越来越复杂,前端单元测试也越来越受重视,包括百度在内的一些大厂在面试中也会问到单元测试相关的题目。那么前端应该如何做好单元测试?
什么是单测
单测的定义
来自维基百科的定义:
在计算机编程中, 单元测试(Unit Testing)又称为 模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。
程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
定义里面指出,单元测试针对的是程序的最小单元,因此我们应该针对最小单元来写单测。
单测完善程度的衡量
单测完善程度的衡量用覆盖率来衡量
关于测试覆盖率,我们大致了解下以下几个常见的计算维度:
- 行覆盖率:可执行语句执行的比例
- 函数覆盖率:函数被调用的比例
- 分支覆盖率:判断语句分支被执行的比例
来看下面这个例子:
对于测试用例 foo(4),分支覆盖率只有50%,原因是 foo(4) 只测试了if 为 true 的情况,没有测试到 if 为 false 的情况。
如果想要分支覆盖率达到100%,还应该测试 foo(5)。
为什么需要单测
现状与问题
- 缺乏意识:没有单测意识,很多代码没有单测
- 缺乏设计:模块缺乏设计,相互耦合,写单测困难
- 测试困难:升级公共 API,人工测试验证困难
- 测试不全面:部分代码分支众多,人工测试没办法覆盖所有分支
单测的价值与意义
- 能力建设:一个具备开发经验的开发人员,基本上都会编写单元测试。即便不会,可以通过培训来快速达成。从学习曲线上看,单元测试很容易上手。
- 提升效率:能够通过 mock 数据,及早发现问题,而越早发现Bug,造成的浪费就会越小。
- 追求卓越:单元测试可以充当一个设计工具,它有助于开发人员去思考代码结构的设计,让代码更加有利于测试。
- 测试更全面:能够覆盖 QA 测试覆盖不到的情况,比如各种 if 分支、异常处理。
- 更有信心:升级公共 API 时,如果依赖这个 API 的所有代码单测都能通过,那我们对这次代码升级是更有信心的。
怎样写单测
单测的原则:FIRST
单测需要测什么:RIGHT BICEP
结果正确
这个是最基本的检查,如果单测的运行结果都不正确,那其他的都免谈。
边界条件检查
边界检查尤为重要,很多错误都是发生在边界上。边界检查遵循 CORRECT
- 有序性:如果一组值是有序的,那么在向其中删除或者添加时,需要校验这组值是否仍然是有序的
- 引用/耦合性:对于类的状态、其他对象的状态等需要做一些假设,程序员就需要对代码进行测试,保证在假设未满足的情况下运行良好。
- 存在性:当数据不存在的时候,程序是否运行良好,例如null,空字符串等
- 基数性:这里的基数主要强调的是计数,考虑以“0-1-N 原则”,当数值分别为0、1、N 时,可能出现的结果,其中N 为最大值。
- 时间性:要充分考虑与时间相关的问题,涉及到并发、异步、超时等这些情况需要特别注意,所有事情的发生时间是否在预期范围内。
反向关联检查
- 例:对于加密算法,使用解密算法反向检查其输出,验证解密后明文是否正确。
- 例:对于写操作API,使用读操作API来反向检查验证
交叉检查
- 例: 对性能优化的算法,使用性能差但验证过的算法交叉检查
强制错误条件发生
- 例: Mock依赖系统不可用 或者 Mock系统时间错误
- 例:甚至可以直接throw error,来验证程序对错误的处理
满足性能要求
当某个程序升级的时候,是否会对性能造成影响?这需要我们对程序的性能进行测试
写单测的时机
写单测的时机主要有以下三种,大家可以根据自己的实际情况来进行选择。
具体实现代码之前
即测试驱动开发(TDD):编写某个功能的代码之前先编写测试代码,然后编写使测试通过的功能代码,重复这个过程,通过测试来推动整个开发的进行。
特点:UT 能够更好的实践我们的代码设计。对于某些复杂度较高的接口(甚至是一些私有方法),建议使用 TDD 进行开发。
与具体实现代码同步进行
先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。
特点:每个单测比较简单易懂,可读性可维护性都比较好(重构时单测的改动不大)。
编写完功能代码后
这种单元测试“粒度”会比较粗。对同样的功能代码,采取前两种方案的结果可能是用10个“小”的单测来覆盖,而这种方案写的单测,往往是用1个“大”的单测来覆盖。
特点:逻辑就比较复杂,因为它要测的东西很多,可读性可维护性可能会比较差。
单测的思路
测试工具的分类
在我们开始选择单测工具之前,需要先了解测试工具的几种分类:
- Test runner
- Test framework
- Assertion library
- Testing Plugins
通常需要这几种类别的工具组合一起使用来完成测试,也有测试框架集成了这几类工具,如 Jest
,做到开箱即用。
Test runner
测试运行器,只负责找出代码中所有的单元测试用例,然后启动运行它们,最终获取测试结果。并不关心那些测试用例到底是用什么语言编写的,以及我们究竟采用的是什么框架,它所做的仅仅是运行这些测试而已。
Karma
是 Test runner
Test framework
测试框架,它定义了测试用例的语法和API,以及如何为这些用例编写断言。
Jasmine
Mocha
和Jest
是 Test framework
Testing Plugins
测试插件如Sinon
,使我们能够执行更多不同的测试,如mock、stubs、 fake servers等。
Assertion library
断言指的是一些布尔表达式,在程序中的某个特定点该表达式值为真,判断代码的实际执行结果与预期结果是否一致,而断言库则是讲常用的方法封装起来。
常用断言库主要有:
- assert (TDD) node自带
assert("mike" == user.name);
- expect.js(BDD) expect 风格的断言
expect(foo).to.be("aa");
- should.js(BDD)should 风格的断言
foo.should.be("aa");
- chai(BDD/TDD) - 集成了expect()、assert()和 should 风格的断言
测试框架的选择
以下介绍几种目前流行的测试工具供大家参考,主要有:
- Jest
- Mocha
- Jasmine
- AVA
- Tape
Jest
Jest是目前最流行的框架之一,是Facebook团队构建和维护的JavaScript测试框架,基于 Jasmine,至今已经做了大量修改添加了很多特性。
主要有以下优点:
- 开箱即用配置少,API简单
- 支持断言和仿真
- 支持快照测试
- 在隔离环境下测试
- 优雅的测试覆盖率报告
- 良好的官网文档
Mocha
Mocha是目前使用最多的js测试框架,同时支持前端和后端测试。可以在node.js和浏览器中运行,使异步测试变的简单而有趣。Mocha测试按串行运行,将未捕获的异常映射到正确的测试用例上,从而可以灵活、准确地报告。
主要优点包括:
- 适用于前端和后端
- 支持NodeJS调试器
- 支持任何浏览器
- 支持Object Mock
- 社区成熟用的人多,测试各种东西社区都有示例
但是mocha不提供断言,需要引入断言库,断言库可以选择 chai
sinon
Jasmine
Jasmine可以模仿用户行为,Jasmine可用于测试前端的可见性,单击清晰度以及不同分辨率下的UI响应速度。 Jasmine允许通过延迟和等待时间来自动化用户行为,以模拟实际的用户行为。
主要优点有:
- 几乎为0的外部依赖,适用于网站、Node.js项目或任何JavaScript可以运行的地方
- 开箱即用(支持断言和仿真),API简单
- 支持前端和后端测试
AVA
AVA是一种极简的轻量级测试框架,它利用了Javascript的异步特性,可以并发运行测试。它主要专注于针对基于NodeJS的代码运行测试。
主要优点包括:
- 体积小,运行速度快
- 异步并发运行测试
- 比大多数其他测试框架更快
- 简单语法
- 干净的堆栈跟踪,使得可以检测到任何潜在的错误
Tape
Tape 架构上与AVA非常相似,它不支持全局变量,因此需要在每个测试文件中require Tape。
主要优点为:
- 体积最小,只提供最关键的东西
- 只提供最底层的 API,为开发人员编写测试用例提供完全的自由
- 支持ES6,Typescript和coffee脚本标准
- 支持大多数现代浏览器上的测试执行
选择哪种测试框架不是非黑即白的,每种测试框架都有各自的优缺点,选择哪种框架可以根据项目实际情况及个人习惯来定。
个人更倾向于 Jest
,因为它容易上手,开箱即用,功能全面,官方文档很详细,对于新手来说使用成本很低。