之前有分享过UI组件库搭建思路和组件如何实现,今天分享的内容是我认为开发组件库中'最'.repeat(1e8)
有(zei)意(ma)思(fan)的部分-单元测试,目前我的UI组件库覆盖率为92%,功能组件部分单侧全部覆盖,工具类部分还没有完全覆盖。报告如下所示:

前言
本篇大致分为三部分内容,第一部分浅显的介绍单元测试,让大家初步了解它的作用及优点。第二部分介绍单元测试的技术选型和在项目中的实战运用,会举一个简单的例子介绍怎么使用它。第三部分介绍如何使用集成服务工具提高我们的工作效率。
希望你阅读完本篇文章之后,能让你对单元测试这个概念有一个初步的了解。接下来我们进入正片部分。
正片
首先提出两个问题。
什么是单元测试?
英文叫 Unit Testing,又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
需要注意以下几种情况:
- 需要访问数据库的测试不叫单元测试;
- 需要访问网络的测试不叫单元测试;
- 需要访问文件系统的测试不叫单元测试。
为什么需要单元测试?
单元测试我总结出以下三个优点。
1. 可能会测出功能的隐藏bug
我在写单元测试的时候,遇到过不止一次我认为输出结果应该为 a
,但是结果却为 b
的情况。这时你就要重新审视你的代码了。
2. 保证代码重构的安全性
组件库中每一个组件都可能会重构或者更新迭代,如果单元测试覆盖率高的话,修改代码之后就越可能会发现潜在的问题。比如某些功能代码不小心删掉了。这样会导致用户更新最新版本时,缺少了之前使用过的功能,产生一些疑惑。
3. 易于测试的代码,源于好的设计
一句话,当我开始为组件库添加单元测试的时候,我才意识到我写的代码是有多么的烂。
除了这些还有一种设计方法论叫 测试驱动开发(TDD),它是敏捷开发中的一项核心实践和技术,在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么样的代码。另外有一种设计方法论叫 行为驱动开发(BDD)。 感兴趣的同学可以自行查阅。
接下来进入第二部分,我们如何来使用单元测试。
技术选型
单元测试用到的工具大致分为三部分:分别为管理工具、测试框架、断言库。
我选用的是Karma、Mocha 和 Chai,接下来分别讲一下他们仨的作用。
Karma
是一个基于 Node.js 的 JavaScript 测试执行过程管理工具,又称 Test Runner
。常用的管理工具还有 Jest
等。
Mocha
是一个功能丰富的前端测试框架。所谓"测试框架",就是运行测试的工具。通过它,可以为 JavaScript 应用添加测试用例,从而保证代码的质量。Mocha
既可以基于 Node.js 环境运行也可以在浏览器环境运行。常用的测试框架还有 Asmine
、 Jasmine
等。
Chai
是一个断言库,类似于 Node 的内置断言。通过提供许多可以针对代码运行的断言,它使测试变得更加容易。不同断言库的语法都大同小异,如下图对比 expect
、should
和 assert
三种断言库的区别:
expect(a).to.equal(b) // expect
a.should.equal(b) // should
assert.equal(a, b) // assert
上面的例子中,a
代表输出的结果,b
代表你想要的结果,三个测试示例都是判断 a
是否等于 b
。
接下来介绍在项目中真实的示例。
单元测试示例
举例 Switch
组件如下图:

图中有两部分信息,分别为四个测量维度和标记。
- 测量维度
行覆盖率(line coverage):是否每一行都执行了
函数覆盖率(function coverage):是否每个函数都调用了
分支覆盖率(brancch coverage):是否每个if代码块都执行了
语句覆盖率(statement coverage):是否每个语句都执行了
- 标记
'E':'else path not taken', 出现在if/else语句,表示经过了if测试,但没经过else测试。
'I':'if path not taken', 出现在if/else语句,没有经过if测试。
'x(N)':该行执行经过几次。
未执行的行或代码段将用红色突出显示
我们看一下 Switch
单元测试代码。
import Vue from 'vue'
import { destroyVM, createVue, createTest } from '../util'
import Switch from 'packages/switch'
describe('Switch', () => {
let vm
afterEach(() => {
destroyVM(vm)
})
it('use', () => {
Vue.use(Switch)
expect(Vue.component(Switch.name))
.to.be.a('function')
})
it('disabled', () => {
vm = createTest(Switch, {
disabled: true
}, true)
let switchElm = vm.$el.querySelector('.owl-switch-input')
expect(switchElm.disabled).to.be.true
})
it('init callback event', done => {
const btnHandler = sinon.spy()
vm = createVue({
template: `
<owl-switch v-model="val"
:init-callback="true"
:color="color"
@callback="handle">
</owl-switch>
`,
data: {
val: false,
color: '#584628',
},
methods: {
handle () {
return btnHandler()
}
}
})
expect(btnHandler).to.be.called
setTimeout(() => {
vm.val = true
done()
})
})
})
测试框架的语法结构为 describe
、it
、expect
,一个测试文件可以有多个描述,it
是定义了一条测试用例,第二个参数为一个回调函数,函数里可以使用断言库所提供的返回结果来判断该测试用例是通过还是失败。
解读一下以上的代码,我创建三条测试用例,第一条测试用例 Switch
组件是否能正常注入;第二条测试用例判断给组件设置为 disabled
时,该组件是否被禁用;第三条测试用例主要测试组件的回调事件是否被执行,如果你有需要延迟调用的逻辑时,需要用到 done
方法。
这样一个简单组件的单侧就写完了。因为功能很少,所以需要写的测试代码也很少。其实有一些复杂的组件会有7到8个测试用例或者更多。
接下来介绍最后一个部分-集成服务工具
集成服务工具
我们在工作中除了开发功能时,写代码需要花费很多时间,构建(build)和测试(test)项目时,也花费了大量的时间。使用构建和测试的自动化工具,会帮助我们节省一部分时间,从而间接地提高我们的工作效率。
介绍两款我正在使用的集成服务工具,Travis CI 和 Codecov。
Travis CI
Travis CI 提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 Github 上面的项目,只要有新的代码,就会自动抓取。然后,它提供了一个运行环境,可执行测试、完成构建,还能部署到服务器。
持续集成指的是只要代码有变更,就自动运行构建和测试,反馈运行结果。确保符合预期以后,再将新代码"集成"到主干。
持续集成的好处在于,每次代码的小幅变更,就能看到运行结果,从而不断累积小的变更,而不是在开发周期结束时,一下子合并一大块代码。
这里我只做简单的介绍,使用教程可以点击了解更多查看。
Codecov
Codecov 提供高度集成的工具来分组、合并、归档和比较覆盖率报告。Codecov 与 Travis CI 一样都支持 Github 账号登录,同样会同步 Github 中的项目。
使用时,先进行安装 npm i codecov -D
,在 .travis.yml
文件中添加 codecov
执行命令即可。这样在每次更改代码时,Travis CI 会执行单侧,同时也会执行 codecov
,把结果推给 Codecov。
他们都提供了各自 badge,让你的项目显得更加专业。


结语
到这里关于单元测试部分已经浅显的讲完了,如果你也想在工作中尝试做单元测试的话,需要自己进行大量的实践及运用。这个过程中避免不了挖坑和填坑的重复操作。我认为能坚持到最后的不只需要有坚强的毅力,也不只需要有丰富的知识储备,而是需要对它有真实的热爱。祝大家学习进步。