Vue组件库工程探索与实践之单元测试

1,360 阅读9分钟

本文是《Vue组件库工程探索与实践》系列文章第三篇,聊聊组件库单元测试和持续集成功能的实现。

单元测试是软件工程领域的一个重要概念,指对软件中的最小可测试单元进行检查和验证,它是代码正确性验证的最重要的工具。单元测试也是组件库实现自动化测试与集成的基础。

单元测试会封闭执行最小化单元的代码,使添加新功能和追踪问题更容易。对代码进行单元测试有很多好处:

  • 可节省手动测试的时间
  • 有助于减少开发新特性时产生的副作用
  • 有利于改进设计和重构
  • 有助于提升大团队中复杂基础代码的可维护性

前端小伙伴们对单元测试多多少少都有一定的了解,但真正有实践经验的并不太多。我想可能与业务项目工期紧张、需求变化比变脸还快、对单元测试的意义认识不够等方面因素有关。当然,也不是所有项目都需要单元测试。

程序猿是这么死的

不过,随着时间的推移,阅历的累积,或许有一天你会真正意识到它的价值,就像李宗盛的歌。

少年不听李宗盛,听懂已是不惑年

对于组件库这种可复用的公共代码来说,其对可靠性、可维护性的要求较普通业务类项目代码更高,引入单元测试是十分必要的。这里分享一下我们在Vue组件库的单元测试方面的探索与实践。

限于篇幅,本文重点谈Vue组件库的单元测试功能及实现,不会过多介绍单元测试基础知识。

首先我们需要明确,Vue组件库的单元测试,主要还是针对其中的Vue组件。

Vue Test Utils

为了方便对 Vue 单文件组件进行测试,Vue.js 官方提供了测试工具库 Vue Test Utils,它提供了一系列方法用于测试Vue组件。

Vue Test Utils 通过把组件隔离挂载,模拟必要的输入 (prop、注入和用户事件) 和对输出 (渲染结果、触发的自定义事件) 的断言来测试 Vue 组件。被挂载的组件会返回到一个包裹器内,而包裹器会暴露很多封装、遍历和查询其内部的 Vue 组件实例的便捷的方法。此外,Vue Test Utils 还提供了模拟用户交互的相关方法。

可见,Vue Test Utils 是 Vue 组件单元测试的必备工具,需要安装。

$ npm install --save-dev @vue/test-utils

测试运行器

测试运行器 test runner 即运行测试的程序,也就是我们通常所说的测试框架。Vue Test Utils 是与测试运行器无关的,主流的测试运行器都支持。

官方推荐两个测试运行器:Jestmocha-webpack

Jest 是 Facebook 开源的一套 JavaScript 测试框架, 它自动集成了断言、JSDOM、覆盖率报告等几乎所有测试工具,功能相当强大。它所需的配置是最少的,不过需要一个能够将Vue单文件组件导入到测试中的预处理器。Vue.js官方提供了 vue-jest 预处理器来处理最常见的单文件组件特性,但仍不是 vue-loader 全部的功能。

mocha-webpack 是知名前端测试框架 Mochawebpack 的一个包裹器。下面这行命令就是它的大致工作原理:先对文件进行 webpack 编译,再用 Mocha 对编译后的文件进行测试。当然,其中还包含很多优化工作。

$ webpack test.js output.js && mocha output.js

因此我们能够通过 webpack + vue-loader 得到完整的Vue单文件组件支持。mocha-webpack 的配置比 Jest 复杂很多。

mocha-webpack2.0.0-beta.0版支持 webpack 4

为了获得完整的Vue单文件组件支持,我们在 NutUI 2.0 项目中选择了 mocha-webpack 测试运行器。

浏览器环境

Vue.js 官方的测试工具库 Vue Test Utils 依赖浏览器环境,而启动真实的浏览器是非常复杂的,因为涉及到不同的平台和版本,兼容性与稳定性不易处理。从另一个角度看,自动测试通常不需要浏览器的用户界面,使用无头浏览器(headless browsers)就能满足需求。

无头浏览器是一种没有图形用户界面的网页浏览器,可以在类似于流行的 Web 浏览器的环境中提供对网页的自动控制,通过命令行界面或使用网络通信来执行。它们能以与浏览器相同的方式渲染和理解 HTML,包括页面布局、颜色、字体选择、JavaScript 与 Ajax 的执行等等。通常用于 Web 的自动化测试等场景。

当下比较流行的无头浏览器有 JSDOMPuppeteerSelenium 等等,我们的项目中选择了 JSDOM 在 Node 虚拟浏览器环境运行测试。

$ npm install --save-dev jsdom jsdom-global
// 在测试的配置文件中
require('jsdom-global')()

断言库

“断言”就是判断代码的实际执行结果与预期结果是否一致,如果不一致就抛出一个错误。所有的测试用例都应该包含至少一条断言。它是编写测试用例的关键。

比如下面这条断言,它的含义是调用 add(1,1) 的结果应该等于2。

expect(add(1,1)).to.be.equal(2);

断言功能由断言库来实现。Jest 框架内置了断言库 expect,而 Mocha不包含断言库。所以选择使用 Mocha框架的时候,我们还需要额外安装一个断言库。

expect 断言风格(如上面的栗子)很接近自然语言,NutUI 2.x 项目的断言库选用的也是 expect。除了 expect,流行的断言库还有 chaiassertshould 等。

$ npm install --save-dev expect

在测试的配置文件中引入 expect,并把它挂到全局对象上,当然也可以在每个测试文件中分别导入。

global.expect = require('expect')

测试环境配置

在确定了测试运行器使用 mocha-webpack,浏览器环境使用 JSDOM,断言库使用 expect 之后,我们就可以开始在组件库脚手架中进行测试工具的安装和配置了。

首先安装测试所需依赖。

$ npm install --save-dev @vue/test-utils mocha mocha-webpack@2.0.0-beta.0 expect jsdom jsdom-global

然后,在项目根目录下新建 test/setup.js 目录及文件,用来设置测试所需的全局环境。我们在该文件中引入 JSDOMexpect

test/setup.js

require('jsdom-global')();
global.expect = require('expect');

接着,在 package.json文件中新增一个测试脚本。

package.json

{
  "scripts": {
    "test": "mocha-webpack --webpack-config webpack.test.conf.js --require test/setup.js src/packages/*/__test__/**.spec.js"
  }
}
  • --webpack-config 指定了测试使用的 webpack 配置文件 webpack.test.conf.js。该文件与生产环境配置文件有些差异,下文会细说。
  • --require 标识确保文件 test/setup.js 在测试之前运行,因此我们可以在该文件中设置测试所需的全局环境。
  • 最后一个参数是该测试包所涵盖的所有测试文件的聚合。

测试覆盖率

测试覆盖率是对测试完全程度的度量,是由测试需求、测试用例的覆盖或已执行代码的覆盖表示的。

一个统计 JavaScript 单元测试覆盖率的知名工具是 istanbul,它以土耳其最大城市伊斯坦布尔命名,因为土耳其地毯世界闻名,而地毯是用来覆盖的。

伊斯坦布尔

我们来看下在 mocha-webpack框架下,如何使用 istanbul统计测试覆盖率。

首先,我们需要安装 nycistanbul-instrumenter-loader:

$ npm install --save-dev nyc istanbul-instrumenter-loader
  • nyc: istanbul的命令行工具。
  • istanbul-instrumenter-loader: 使用钩子(hooks)包装代码以在代码执行时跟踪覆盖率的loader。

接下来,修改 package.json文件 scripts字段下 test脚本,同时增加 nyc的相关配置项:

package.json

{
  ...
  "scripts": {
    "test": "cross-env NODE_ENV=test nyc --reporter=lcov --reporter=text  mocha-webpack --webpack-config build/webpack.test.conf.js --require test/setup.js src/packages/*/__test__/**.spec.js"
    ...
  },
  "nyc": {
    "include": [
      "src/packages/**/*.vue"
    ],
    "instrument": false,
    "sourceMap": false
   },
   ...
}

test脚本中 --reporter=lcov --reporter=text是配置 nyc同时输出lcov (lcov.info + html)和文本形式的覆盖率报告。

scripts字段下方 nyc的配置项中:

  • include 用来指定被测试源文件的位置。
  • 禁用 nycinstrumentsourceMap 选项,因为这些工作应该由 loader 负责。

接下来需要把 istanbul-instrumenter-loader加到 webpack 的配置文件中。上文提到,我们给单元测试工作指定的 webpack 配置文件是 webpack.test.conf.js,它与生产环境的配置文件有些差异,我们先把生成环境的配置文件 webpack.prod.conf.js 导入,再把这些差异 merge 进去。

webpack.test.conf.js

const path = require('path');
const prodConf = require('./webpack.prod.conf.js');
const merge = require('webpack-merge');

module.exports = merge(prodConf, {
    module: {
        rules: [
            {
                test: /\.(js|ts)/,
                use: {
                    loader: 'istanbul-instrumenter-loader',
                    options: { esModules: true }
                },
                include: path.resolve(__dirname, '../src/packages/')
            },
        ],
    },
    devtool: 'inline-cheap-module-source-map',
    externals: [require('webpack-node-externals')()]
});
  • 我们给 js/ts 类型的文件新增了 loader: istanbul-instrumenter-loader,要注意的是,该 loader 必须放在数组的第一个,以确保它最后一个应用。
  • sourceMapmocha-webpack中必须通过内联方式获取,所以 devtool 选项推荐的值为 inline-cheap-module-source-map
  • 为了提升测试的启动速度,我们可以通过 webpack-node-externals 外置所有的 NPM 依赖。

至此,整个项目的自动化测试功能及覆盖率统计配置基本完成,我们在 src/packages/ 目录下的每个组件的目录下新建一个__test__目录,该目录下的单元测试文件以 .spec.js 作为扩展名。

目录结构

准备好组件的单元测试文件之后,在终端执行 npm test 即可启动测试。测试的过程中,终端会展示每个测试用例的测试结果。

单元测试

测试结束之后,终端会以文本形式展示出本次测试的覆盖率报告,同时 /coverage目录下也会生成测试覆盖率报告文件(Icov.info+html)。

测试覆盖率

持续集成

持续集成(Continuous Integration, CI)是一种软件开发实践,即每次代码的集成都通过自动化的构建(包括编译/发布/自动化测试)来验证,从而尽早的发现集成错误。也就是说,只要代码有变更,就自动运行构建和测试,确保符合预期后,再将新代码集成到主干。它的核心措施是,在代码集成到主干之前,必须通过自动化测试。

持续集成的意义在于:

  • 每次代码提交都进行自动构建和测试,有助于尽早的发现问题和解决问题,减少风险。
  • 自动化的构建和测试可以减少人重复的工作,节约时间,降低成本。
  • 有助于提升项目质量。

Travis CI 是在线托管的 CI 服务,使用 Travis 来进行持续集成。它对于开源项目是免费的,支持绑定 Github 上面的项目。只要有新的代码,就会自动抓取。然后,提供一个运行环境,自动进行构建和测试,还支持部署到服务器。

我们来看下 Travis CI的基本用法。

首先,访问 Travis CI 官网并使用 Github 账户登录。 然后,点击其网站右上角的个人头像,网页会列出 Github 上我们和我们所在的组织的所有代码仓库。打开需要进行 CI 的仓库右侧的开关即可。

激活仓库

接下来,我们需要在这个代码仓库的根目录放置一个名为 .travis.ymlTravis CI 配置文件。NutUI 2.x项目的配置文件内容如下:

sudo: required
language: node_js
node_js:
  - '8'
script:
  - npm test
  - npm run coveralls
  • sudo: required 表示需要 sudo 权限。
  • language: node_js 指定运行环境为 Node 。
  • node_js 字段用来指定 Node 版本。
  • script 字段用来指定构建或者测试的脚本。

Travis 的运行流程包含两个阶段:install(安装依赖)和 script(执行脚本)。对于 Node 项目来说,installscript阶段都有默认脚本,如不需要修改,可以省略不写。

  • install的默认脚本是:npm install
  • script的默认脚本是:npm test

Travis CI 还支持自动部署,但不是必须的。我们的组件库没有用到,这里不多说。

配置完成之后,每次往 Github 的该仓库 push 代码,都会触发 CI。登录 Travis CI网站可以看到结果。

NutUI某次自动构建结果

我们还可以从 Travis CI 网站获取一个关联该仓库 CI 结果的徽标,放在我们项目的 README.md 文件中,这样即便不登录 Travis CI 网站,我们也可以通过该徽标知晓 CI 结果了。

CI结果徽标

测试覆盖率上报

查看 NutUIREADME.md文件,会发现除了上文提到的CI结果徽标,还有一个展示测试覆盖率的徽标。

这个徽标是通过 coveralls.io获取的。 coveralls.io 提供测试覆盖率的追踪服务,我们可以把测试覆盖率报告上报给 coveralls.io ,它会基于接收到的数据生成一个测试覆盖率徽标。

coveralls.io 支持 Github 上的项目,也可以与 Travis CI集成。关于它的具体使用,限于篇幅,这里就不展开了,有兴趣的小伙伴可以阅读其官方文档。


好了,这篇文章先聊到这里。如果对具体实现细节感兴趣,可以查看 NutUI 2.x 项目的源码,也欢迎各位老铁Star,赠人Star,手有余香~

链接