方案选型
参考 Vue Test Utils 的官方文档和Vue测试指南的内容,目前 Vue2.x 单元测试方案主要有以下3种:
- 方案1:Jest(省心全家桶)
- 方案2:Mocha + 【mochapack & jsdom】 + 【chai & sinon & istanbul】
- 方案3:Mocha + 【Karma】 + 【chai & sinon & istanbul】
先简单介绍一下:Mocha 是常用的测试框架,提供 describe 和 it 函数;Karma 是测试运行器,相当于一个离线浏览器,相比于 jsdom 来说,由于是真实的浏览器环境因此测试结果更可靠;chai 是断言库,主要使用它的 expect 函数;sinon 提供函数的 fake/spy/stub/mock 功能,用来模拟单测用到的函数;最后的 istanbul 主要用来计算测试覆盖率并生成报告。我用括号把它们简单分了个组,方便理解。
第一种方案的 Jest(来自 React 家) ,自带断言库、运行器以及覆盖率计算功能,实现了开箱即用的良好体验,是当前最流行的方案。
第二种方案最大的优点是利用 mochapack 将 webpack 编译后的代码整合到框架和运行器中,从而可以完全支持所有 webpack 和 vue-loader 的功能,相比 Jest 更加灵活但是部署和配置的成本较高,而且实际使用起来,也很少会用到基础功能以外的东西。
第三种方案和第二种方案基本一致,主要的区别在于运行器 Karma 使用的是真实的浏览器环境(chrome)而非 jsdom 模拟的浏览器环境,因此会更贴近真实场景,ElementUI 即是采用的这个方案。
经过对三种方案的部署和测试,最终不得不服软,相比于灵活的组装,省心全家桶还是太香了。话虽如此,其实主要原因还是方案二&三或多或少都会遇到一些问题,解决起来不是那么方便。
 框架部署
首先大致介绍一下各个方案的部署方法。
 方案1:Jest
首先安装所有需要用到的模块:
|  | # 核心npm i -D jest @vue/test-utils
 
 # 处理 vue 单文件组件
 npm i -D @vue/vue2-jest
 
 # 提供 babel 支持
 npm i -D babel-jest babel-core@^7.0.0-bridge.0
 
 # 支持 CSS Modules,避免 Babel 解析出错
 npm i -D identity-obj-proxy
 
 | 
在项目目录下新建 jest.config.js(或者也可以直接写到 package.json 的 jest 块里面),配置的主要内容如下:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 
 | module.exports = {
 testEnvironment: 'jsdom',
 
 moduleFileExtensions: ['js', 'json', 'vue'],
 
 transform: {
 '.*\\.(vue)$': '@vue/vue2-jest',
 '.*\\.(js)$': 'babel-jest',
 },
 
 moduleNameMapper: {
 '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
 '<rootDir>/test/mocks/fileMock.js',
 '\\.(css|less)$': 'identity-obj-proxy',
 '^@/(.*)$': '<rootDir>/src/$1',
 },
 
 collectCoverage: false,
 collectCoverageFrom: [
 '**/*.{js,vue}',
 '!**/node_modules/**'
 ],
 };
 
 | 
 方案2:Mocha & mochapack & jsdom
首先安装所有需要用到的模块:
| 12
 3
 4
 5
 6
 7
 
 | npm i -D @vue/test-utils mocha mochapack
 # 模拟浏览器环境
 npm i -D jsdom jsdom-global
 
 # 断言库、函数存根、覆盖率
 npm i -D chai sinon nyc istanbul-instrumenter-loader
 
 | 
然后在 package.json 中定义命令:
| 12
 3
 4
 5
 6
 
 | {
 "scripts": {
 "test": "mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
 }
 }
 
 | 
在 test/setup.js 中写入:
| 12
 3
 4
 
 | require('jsdom-global')()
 
 global.expect = require('chai').expect;
 
 | 
 方案3:Mocha & Karma
首先安装所有需要用到的模块:
| 12
 3
 4
 5
 6
 7
 8
 
 | # 核心npm i -D @vue/test-utils karma karma-chrome-launcher karma-mocha mocha karma-webpack karma-sourcemap-loader
 
 # 接着是增加对断言库和函数存根的支持:
 npm i -D karma-sinon-chai sinon chai sinon-chai
 
 # 最后是增加对计算覆盖率的支持:
 npm i -D babel-plugin-istanbul karma-coverage karma-spec-reporter
 
 | 
在项目目录下新建 karma.config.js,主要内容如下所示:
注意:配置中 frameworks 部分使用了 sinon-chai,这个框架使用的是 karma-sinon-chai 的包,由于新版本的 nodejs 限制了 require.resolve 函数使用的场景,会导致测试运行时报错,具体原因可见 https://github.com/nodejs/node/issues/33460。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 
 | var webpackConfig = require('./webpack/webpack.config.dev');
 module.exports = function(config) {
 config.set({
 
 basePath: '',
 
 
 
 browsers: ['Chrome'],
 
 
 
 frameworks: ['mocha', 'sinon-chai'],
 
 
 files: [
 'test/**/*.spec.js'
 ],
 
 
 exclude: [],
 
 
 
 preprocessors: {
 '**/*.spec.js': ['webpack', 'sourcemap'],
 },
 
 
 
 
 reporters: ['spec', 'coverage'],
 
 coverageReporter: {
 dir: './coverage',
 reporters: [
 { type: 'lcov', subdir: '.' },
 { type: 'text-summary' }
 ],
 }
 
 
 webpack: webpackConfig,
 });
 };
 
 | 
代码中未提到的配置可到 Karma 官方文档 查看。
 用例编写
开始编写用例前我们需要认真思考一个问题:哪些代码需要测试?
现在的前端的代码主要还是以页面脚本为主,独立的纯逻辑代码比较少。同时由于前端是直接对接用户的,需求变更会比较频繁,如果对每个页面的每个点都进行测试,那先不说需要多少时间去开发用例,很可能刚完成用例的编写就突然遇上需求变更,之前写的用例就全部作废了,这样一来导致开发效率和写用例的积极性都会受到沉重打击。
然而也大可不必因噎废食,前端的单元测试还是需要辩证地去看待,正是由于前端页面的代码经常变动的缘故,如果引入合适的测试代码,那么缺陷代码将会有更大可能被提前发现,其潜在收益未来可期。
至此,考虑到现在的前端开发基本都是通过组件来完成的,组件可以划分成功能独立且稳定的公共组件和频繁变动的业务组件,业务组件往往都是通过组合公共组件来完成需求的开发,因此我们完全可以只给公共组件编写用例,然后考虑到使用频率,可能同时需要给几个入口页面最好加上测试。
由于前端多页面代码的特点,代码覆盖率显得没那么重要,同时也要避免面向测试写用例的问题。从 BDD 的角度看,我只需要保证当前参与测试的元素和元素的交互没有问题就好。
那么测试内容应该包括:
- 页面上的关键元素的存在性;
- 页面上的关键动作的可交互性;
- 关键事件和方法。
在开始编写测试用例前,先理解两个概念,这两个概念贯穿了用例编写的始终:
- mock:模拟真实功能中的具体步骤,关注点是行为;
- stub:模拟真实功能返回的最终结果,关注点是状态;
建议在测试目录下新建一个 index.js 入口文件,用于定义全局的 mocks 和 stubs:
| 12
 3
 4
 5
 6
 7
 8
 
 | import { config } from '@vue/test-utils';
 
 config.mocks['$t'] = (msg) => msg;
 
 
 config.stubs['transition'] = true;
 config.stubs['transition-group'] = true;
 
 | 
然后 jest.config.js 中加上:
| 12
 3
 
 | setupFiles: ['<rootDir>/test/index.js'],
 
 
 | 
我们的用例基本结构如下所示:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 
 | import { mount, createLocalVue } from '@vue/test-utils';import Vuex from 'vuex';
 import ElementUI from 'element-ui';
 
 import xxx from 'xxx';
 
 
 const localVue = createLocalVue();
 localVue.use(Vuex);
 localVue.use(ElementUI);
 
 
 const wrapper = mount('xxx');
 
 
 describe('Element existence check', () => {
 
 });
 
 
 describe('Interactive action check', () => {
 
 });
 
 
 describe('Crucial event and method check', () => {
 
 });
 
 | 
 参考资料