Vuejs前端UT实践

方案选型

参考 Vue Test Utils 的官方文档[1]和Vue测试指南[2]的内容,目前 Vue2.x 单元测试方案主要有以下3种:

  • 方案1:Jest(省心全家桶)
  • 方案2:Mocha + 【mochapack & jsdom】 + 【chai & sinon & istanbul】
  • 方案3:Mocha + 【Karma】 + 【chai & sinon & istanbul】

先简单介绍一下:Mocha 是常用的测试框架,提供 describeit 函数;Karma 是测试运行器,相当于一个离线浏览器,相比于 jsdom 来说,由于是真实的浏览器环境因此测试结果更可靠;chai 是断言库,主要使用它的 expect 函数;sinon 提供函数的 fake/spy/stub/mock 功能,用来模拟单测用到的函数;最后的 istanbul 主要用来计算测试覆盖率并生成报告。我用括号把它们简单分了个组,方便理解。

第一种方案的 Jest(来自 React 家) ,自带断言库、运行器以及覆盖率计算功能,实现了开箱即用的良好体验,是当前最流行的方案。

第二种方案最大的优点是利用 mochapack 将 webpack 编译后的代码整合到框架和运行器中,从而可以完全支持所有 webpack 和 vue-loader 的功能,相比 Jest 更加灵活但是部署和配置的成本较高,而且实际使用起来,也很少会用到基础功能以外的东西。

第三种方案和第二种方案基本一致,主要的区别在于运行器 Karma 使用的是真实的浏览器环境(chrome)而非 jsdom 模拟的浏览器环境,因此会更贴近真实场景,ElementUI 即是采用的这个方案。

经过对三种方案的部署和测试,最终不得不服软,相比于灵活的组装,省心全家桶还是太香了。话虽如此,其实主要原因还是方案二&三或多或少都会遇到一些问题,解决起来不是那么方便。

框架部署

首先大致介绍一下各个方案的部署方法。

方案1:Jest

首先安装所有需要用到的模块:

1
2
3
4
5
6
7
8
9
10
11
# 核心
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 块里面),配置的主要内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
// 指定测试环境,默认是 node,测试 webapp 时需要改为 jsdom
testEnvironment: 'jsdom',
// 需要处理的文件类型
moduleFileExtensions: ['js', 'json', 'vue'],
// 提供代码转译支持
transform: {
'.*\\.(vue)$': '@vue/vue2-jest',
'.*\\.(js)$': 'babel-jest',
},
// webpack别名支持
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

首先安装所有需要用到的模块:

1
2
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 中定义命令:

1
2
3
4
5
6
// package.json
{
"scripts": {
"test": "mochapack --webpack-config webpack.config.js --require test/setup.js test/**/*.spec.js"
}
}

test/setup.js 中写入:

1
2
3
4
require('jsdom-global')()

// 方便全局使用 expect
global.expect = require('chai').expect;

方案3:Mocha & Karma

首先安装所有需要用到的模块:

1
2
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。

1
2
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({
// 解析 files 和 exclude 时使用的根路径
basePath: '',

// 运行测试使用的浏览器
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],

// 运行测试使用的框架
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'sinon-chai'],

// 测试加载的文件
files: [
'test/**/*.spec.js'
],

// 需要排除的文件
exclude: [],

// 预处理器,按顺序执行
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'**/*.spec.js': ['webpack', 'sourcemap'],
},

// 测试报告
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['spec', 'coverage'],
// 覆盖率报告配置
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
],
}

// 加载 webpack 配置
webpack: webpackConfig,
});
};

代码中未提到的配置可到 Karma 官方文档 查看。

用例编写

开始编写用例前我们需要认真思考一个问题:哪些代码需要测试?

现在的前端的代码主要还是以页面脚本为主,独立的纯逻辑代码比较少。同时由于前端是直接对接用户的,需求变更会比较频繁,如果对每个页面的每个点都进行测试,那先不说需要多少时间去开发用例,很可能刚完成用例的编写就突然遇上需求变更,之前写的用例就全部作废了,这样一来导致开发效率和写用例的积极性都会受到沉重打击。

然而也大可不必因噎废食,前端的单元测试还是需要辩证地去看待,正是由于前端页面的代码经常变动的缘故,如果引入合适的测试代码,那么缺陷代码将会有更大可能被提前发现,其潜在收益未来可期。

至此,考虑到现在的前端开发基本都是通过组件来完成的,组件可以划分成功能独立且稳定的公共组件和频繁变动的业务组件,业务组件往往都是通过组合公共组件来完成需求的开发,因此我们完全可以只给公共组件编写用例,然后考虑到使用频率,可能同时需要给几个入口页面最好加上测试。

由于前端多页面代码的特点,代码覆盖率显得没那么重要,同时也要避免面向测试写用例的问题。从 BDD 的角度看,我只需要保证当前参与测试的元素和元素的交互没有问题就好。

那么测试内容应该包括:

  • 页面上的关键元素的存在性;
  • 页面上的关键动作的可交互性;
  • 关键事件和方法。

在开始编写测试用例前,先理解两个概念,这两个概念贯穿了用例编写的始终:

  • mock:模拟真实功能中的具体步骤,关注点是行为;
  • stub:模拟真实功能返回的最终结果,关注点是状态;

建议在测试目录下新建一个 index.js 入口文件,用于定义全局的 mocks 和 stubs:

1
2
3
4
5
6
7
8
import { config } from '@vue/test-utils';

// mock vue-i18n 的 $t 方法
config.mocks['$t'] = (msg) => msg;

// 对 transition 和 transition-group 两个组件进行存根,避免动画对测试造成影响
config.stubs['transition'] = true;
config.stubs['transition-group'] = true;

然后 jest.config.js 中加上:

1
2
3
// ...
setupFiles: ['<rootDir>/test/index.js'],
// ...

我们的用例基本结构如下所示:

1
2
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';

// 创建本地 Vue 实例用于测试
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(ElementUI);

// 由于是给组件写用例,大部分场景需要使用 mount 而非 shallowMount
const wrapper = mount('xxx');

// 关键元素存在性校验
describe('Element existence check', () => {
// ...
});

// 关键交互动作校验
describe('Interactive action check', () => {
// ...
});

// 关键事件和方法校验
describe('Crucial event and method check', () => {
// ...
});

参考资料