方案选型
参考 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
块里面),配置的主要内容如下:
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 = { 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
首先安装所有需要用到的模块:
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
| { "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')()
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({ 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:
1 2 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
中加上:
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';
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', () => { });
|
参考资料