好的单元测试,对开发速度、项目维护有莫大的帮助。前端的测试工具一直推陈出新,而测试的核心、原则却少有变化。与产品代码一并交付可靠的测试代码,是每个专业开发者应该不断靠近的一个理想之地。本文就围绕测试讲讲,为什么我们要做测试,什么是好的测试和原则,前端单元测试需要什么工具,又如何选择对应的测试框架。
单元测试
单元测试是什么
单元测试又称为模块测试,是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
简单的说,单元测试是一种验证,验证代码功能,方法的实现的正确性。
为什么要做单元测试?
1.重构、重构、重构
TDD的具体实现就是通过红灯->绿灯->重构不断重复,一步一步去健壮我们的代码,所以单元测试的最大的意义也是为了我们今后可以重构我们的代码,只要保证测试的准确,就可以在重构中准确的定位到问题。同时也为以后的开发提供支持,在测试的基础上我们可以重构结构和业务功能。
2.单元测试是最好的注释
写注释是很多程序员都会忽略的一个步骤,或者改了代码你并不会记得去改注释,很多程序员会倾向于把变量名作为注释,但它无法很好的解释内部的逻辑,而测试会提示你那些步骤是可以通过、如何使用的最好文档,更详细的规范了测试目标的边界值与非法值。
3.定位bug,减少bug
测试最直观的体现当然是与bug相关的,单元测试可以通过不同的条件来发现问题在哪里,在一些弱类型的语言中也避免了一些类型检查的低级错误,当然这个现在我们都用TypeScript做到了。
4.被迫的规范组织结构
可能平时我们会把一个方法写的很复杂、一个类写的很大,没有想过如何去组织结构,但如果你想到你即将的测试要如何写的时候,那可能你在开发前必须要想想哪些部分可以提出来了。
前端单元测试怎么做
前端单元测试需要哪些工具
在前端的世界里,至少需要三类工具来进行单元测试:
测试管理工具
测试管理工具是用来组织和运行整个测试的工具,它能够将测试框架、断言库、测试浏览器、测试代码和被测试代码组织起来,并运行被测试代码进行测试。例如Karma
等
测试框架
测试框架是单元测试的核心,它提供了单元测试所需的各种 API,你可以使用它们来对你的代码进行单元测试。例如Mocha
、Jest
、Jasmine
等
断言库
断言库提供了用于描述你的具体测试的 API,有了它们你的测试代码便能简单直接,也更为语义化,理想状态下你甚至可以让非开发人员来撰写单元测试。例如Chai
、Jasmine
、Jest
等
测试框架和测试管理工具的区别
一个提供了测试的框架,一个则提供了测试的运行环境。测试框架编写测试文件,用来做单元测试,管理工具来提供测试文件运行时的真实浏览器模拟环境以及他集成的一些诸如测试覆盖率报告以及文件监听等功能来完善以及自动化测试流程。总而言之,一个用来做单元测试,一个用来将单元测试自动化。如果你只在node中进行单元测试,那么使用测试框架即可,如果想要你的代码在浏览器体验真实的运行效果,那么可以考虑使用测试管理工具
。
前端单元测试到底测什么?
单元测试的重点在于单元,这也是前端单元测试的难点。现在我们大部分使用的框架大多把页面渲染和功能放到了一起,那么哪些才是我们需要测试的单元?从相对的角度来说,一些不会经常变化的功能可以细分成单元进行测试:
- 公共函数
- 公共组件
越底层的代码越有测试的必要
,因为UI的实现会依赖底层代码,例如我们可能用到的一些类似ramda、antd库,都会经过严格的单元测试,如果我们想要在项目中自己实现,就要对这样方法和组件进行测试,业务逻辑一般会跟着项目迭代和更新随时变化,单元测试的意义不大。
如何写好单元测试:好测试的特征
写单元测试仅仅是第一步,下面还有个更关键的问题,就是怎样写出好的、容易维护的单元测试。好的测试有其特征,虽然它并不是什么新的东西,但总需要时时拿出来温故知新。很多时候,开发感觉测试难写、难维护、不稳定、价值不大等,可能都是因为单元测试写不好所导致的。那么我们就来看看,一个好的单元测试,应该遵循哪几点原则。
首先,我们先来看个简单的例子,一个最简单的 JavaScript 的单元测试长什么样:
// production code
const computeSumFromObject = (a, b) => {
return a.value + b.value
}
// testing code
it('should return 5 when adding object a with value 2 and b with value 3', () => {
// given - 准备数据
const a = { value: 2 }
const b = { value: 3 }
// when - 调用被测函数
const result = computeSumFromObject(a, b)
// then - 断言结果
expect(result).toBe(5)
})
以上就是一个最简答的单元测试部分。但麻雀虽小,五脏基本全,它揭示了单元测试的一个基本结构:准备输入数据、调用被测函数、断言输出结果。任何单元测试都可以遵循这样一个骨架,它是我们常说的 given-when-then 三段式。
为什么说单元测试说来简单,做到却不简单呢?除了遵循三段式,显然我们还需要遵循一些其他的原则。前面说到,我们对单元测试寄予了几点厚望,下面就来看看,它如何能达到我们期望的效果,以此来反推单元测试的特征:
- 安全重构已有代码 -> 应该有且仅有一个失败的理由、不关注内部实现
- 单元测试是最好的注释 -> 表达力极强
- 快速回归 -> 快、稳定
下面来看看这三个原则都是咋回事:
有且仅有一个失败的理由
有且仅有一个失败的理由,这个理由是什么呢?是当输入不变时,当且仅当被测业务代码功能被改动了
时,测试才应该挂掉。为什么这会支持我们重构呢,因为重构的意思是,在不改动软件外部可观测行为的基础上,调整软件内部实现的一种手段。也就是说,当我被测的代码输入输出没变时,任我怎么倒腾重构代码的内部实现,测试都不应该挂掉。这样才能说是支持了重构。有的单元测试写得,内部实现(比如数据结构)一调整,测试就挂掉,尽管它的业务本身并没修改,这样无法支持重构。
一般会出现这种情况,可能是因为是先写完代码再补的测试,或者对代码的接口和抽象不明确所导致。另外,还有一些测试,它需要测试实现代码的执行次序。这也是一种关注内部实现
的测试,这就使得除了业务目标外,还有执行次序
这个因素可能使测试挂掉。这样的测试也是很脆弱的。
表达力极强
表达力极强,讲的是两方面:
- 看到测试时,你就知道它测的业务点是啥
- 测试挂掉时,能清楚地知道业务、期望数据与实际输出的差异
这些表达力体现在许多方面,比如测试描述、数据准备的命名、与测试无关数据的清除、断言工具能提供的比对等。
快、稳定
一般来讲,一个没有依赖、没有 API 调用的单元测试,都能在毫秒级内完成。那么为了达到快、稳定这个目标,我们需要:
- 隔离尽量多的依赖。依赖少,速度就快,自然也更稳定
- 将依赖、集成等耗时、依赖三方返回的地方放到更高层级的测试中,有策略性地去做
- 测试代码中不要包含逻辑,不然无法确定是实现挂了还是测试挂了
前端单元测试工具选择
工欲善其事必先利其器,单元测试应该简单,快速执行,清晰的错误报告,接下来从社区情况,学习成本,文档完善性,稳定等方面分析各工具优劣。
前端单元测试常用框架
jest
Jest是Facebook开源的一款JS单元测试框架,适用于使用以下项目的项目:Babel,TypeScript,Node,React,Angular,Vue等。目前除了Facebook 外,Twitter、Nytimes、Airbnb 也在使用 Jest。Jest 除了基本的断言和 Mock 功能外,还有快照测试、实时监控模式、覆盖度报告等实用功能。同时Jest几乎不需要做任何配置便可使用。
- facebook 坐庄
- 基于 Jasmine 至今已经做了大量修改添加了很多特性
- 开箱即用配置少,API简单
- 支持断言和仿真、快照测试
- 优雅的测试覆盖率报告,基于Istanbul
- 智能并行测试
- 较新,社区不十分成熟
- 全局环境,比如 describe 不需要引入直接用
- 较多用于 React 项目(但广泛支持各种项目)
Mocha
Mocha(发音"摩卡")诞生于2011年,是现在最流行的JavaScript测试框架之一,既可以在浏览器环境下运行,也可以在Node.js环境下运行,主要优点是灵活,可扩展性强。
- 灵活(不包括断言和仿真,自己选对应工具)
流行的选择:chai,sinon - 社区成熟用的人多,测试各种东西社区都有示例
- 测试覆盖率报告
- 提供JavaScript API来运行测试
- 需要较多配置
- 可以使用快照测试,但依然需要额外配置
Jasmine
Jasmine是面向行为驱动开发(BDD)的Javascript单元测试框架。它不依赖于其他任何javascript框架,语法清晰简单,很容易上手写出测试代码
- 开箱即用(支持断言和仿真)
- 全局环境
- 比较'老',坑基本都有人踩过了
AVA
- 异步,性能好
- 简约,清晰
- 快照测试和断言需要三方支持
Tape
- 体积最小,只提供最关键的东西
- 对比其他框架,只提供最底层的 API
总结
Mocha 用的人最多,社区最成熟,灵活,可配置性强易拓展,Jest 开箱即用,里边啥都有提供全面的方案,Tape 最精简,提供最基础的东西最底层的API。
Jasmine vs Jest vs Mocha
社区情况
Jest和Mocha热度较Jasmine高,Jest投票数最低,但应考虑到 Jest 比较年轻,参与投票的时间较短的因素。
Jest热度最高,关注人数也最多
数据来源:stackshare
代码样例
测试的代码
'use strict'
var Math = {
add(a, b) {
return a + b;
}
}
module.exports = Math;
Jasmine
var math = require('../Math');
describe("Math", function() {
var firstOperand;
var secondOperand;
beforeEach(function() {
firstOperand = 2;
secondOperand = 3;
});
it("should add two numbers", function() {
var result = math.add(firstOperand, secondOperand);
expect(result).toEqual(firstOperand + secondOperand);
});
});
Jest
jest.unmock('../Math'); // unmock to use the actual implementation of Math
var math = require('../Math');
describe("Math", function() {
var firstOperand;
var secondOperand;
beforeEach(function() {
firstOperand = 2;
secondOperand = 3;
});
it("should add two numbers", function() {
var result = math.add(firstOperand, secondOperand);
expect(result).toEqual(firstOperand + secondOperand);
});
});
Mocha
var assert = require('assert'); // nodejs 内建断言
var math = require('../Math');
describe("Math", function() {
var firstOperand;
var secondOperand;
beforeEach(function() {
firstOperand = 2;
secondOperand = 3;
});
it("should add two numbers", function() {
var result = math.add(firstOperand, secondOperand);
assert.equal(result, firstOperand + secondOperand);
});
});
代码写法大体相同,分为3点:
- describe:"测试组",也称测试块,表示我要进行一系列测试,相当于一个group
- it:"测试项",也称测试用例,表示这是"一系列测试"中的一项,相当于item,如何测试?测试逻辑?都是在it的回调函数中实现的
- expect()是单元测试的构建块,被称为“断言(assertion)”
结合项目-Vue & Angular & React
Vue
- 脚手架安装:官方脚手架提供了两种测试框架 jest,mocha,可在脚手架初始时进行选择,结合了vue-test-utils 这个官方的测试库。
Vue Test Utils
是 Vue.js 官方的单元测试实用工具库,为jest/mocha和vue提供了一个桥梁,暴露出一些接口,让我们更加方便的通过Jest为Vue应用编写单元测试。 - 项目引入:根据官方文档安装环境
Angular
- 脚手架安装: angular cli会自动安装配置单元测试,运用karma和Jasmine。
- 项目引入: 可以使用其它的测试库和测试运行器来对 Angular 应用进行单元测试。
React
自动集成jest框架,提供ReactTestUtils
可搭配你所选的测试框架,轻松实现 React 组件测试。
框架选择
选择测试框架并不是非黑即白的事儿,就像你并不能证明PHP不是最好的语言,需结合项目进行方案抉择。
- 如果考虑扩展性,可选择mocha,可进行灵活配置,社区也比较成熟
- 想要快速上手,可选择Jest,开箱即用,功能全面,无需进行太多配置,且github关注度高