网易云团队前端单元测试技术方案总结(二)
AMDAMD 是 RequireJS 推广过程中流行的一个比较老的规范,目前无论浏览器还是 Node 都没有默认支持。AMD 的标准定义了 define 和 require函数,define用来定义模块及其依赖关系,require 用以加载模块。例如:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>Document</title>
+ <script
+src="https://requirejs.org/docs/release/2.3.6/minified/require.js"></script>
+ <script src="./index.js" />
</head>
<body></body>
</html>// index.js
define('moduleA', ['https://some/of/cdn/path'], function() {
return { name: 'moduleA' };
});
define(function(require) {
const fs = require('fs');
return fs;
})
define('moduleB', function() {
return { name: 'module B' }
});
require(['moduleA', 'moduleB'], function(moduleA, moduleB) {
console.log(module);
});这里使用了RequireJS 作为 AMD 引擎, 可以看到 define 函数会定义当前依赖了哪些模块并将模块加载完成后异步回调给当前模块,这种特性使得 AMD 尤为适合浏览器端异步加载。
我们可以使用 webpack 打包一份 amd 模块看下真实代码
// entry.js
export default function sayHello() {
return 'hello amd';
} // webpack.config.js
module.exports = {
mode: 'development',
devtool: false,
entry: './entry.js',
output: {
libraryTarget: 'amd'
}
}最终生成代码(精简了不相关的逻辑)
// dist/main.js
define(() => ({
default: function sayHello() {
return 'hello amd';
}
}));在浏览器/Node 中想要使用 AMD 需要全局引入 RequireJS,对单元测试而言比较典型的问题是在初始化 karma 时会询问是否使用 RequireJS ,不过一般现在很少有人使用了。
CommonJS
可以缩写成CJS , 其 规范 主要是为了定义 Node 的包格式,CJS 定义了三个关键字, 分别为 require,exports, module, 目前几乎所有Node 包以及前端相关的NPM包都会转换成该格式, CJS 在浏览器端需要使用 webpack 或者 browserify 等工具打包后才能执行。
ES Module
ES Module 是 ES 2015 中定义的一种模块规范,该规范定义了 代表为 import 和 export ,是我们开发中常用的一种格式。虽然目前很多新版浏览器都支持<script type="module"> 了,支持在浏览器中直接运行 ES6 代码,但是浏览器不支持 node_modules ,所以我们的原始 ES6 代码在浏览器上依然无法运行,所以这里我暂且认为浏览器不支持 ES6 代码, 依然需要做一次转换。
下表为每种格式的支持范围,括号内表示需要借助外部工具支持。
单元测试要在不同的环境下执行就要打不同环境对应的包,所以在搭建测试工具链时要确定自己运行在什么环境中,如果在 Node 中只需要加一层 babel 转换,如果是在真实浏览器中,则需要增加 webpack 处理步骤。
所以为了能够在 Node 环境的 Mocha中使用 ES Module 有两种方式
1.Node 环境天生支持 ES Module(node version >= 15)
2.使用 babel 代码进行一次转换
第一种方式略过,第二种方式使用下面的配置
npm install @babel/register @babel/core @babel/preset-env --save-dev// .mocharc.js
+ require('@babel/register');
global.expect = require('chai').expect;// .babelrc
+ {
+ "presets": ["@babel/preset-env" ,“@babel/preset-typescript”]
+ }同样地如果在项目中用到了 TypeScript, 就可以使用ts-node/register 来解决,因为 TypeScript本身支持 ES Module 转换成 CJS, 所以支持了 TypeScript后就不需要使用 babel 来转换了。(这里假设使用了 TypeScript 的默认配置)
npm install ts-node typescript --save-dev // .mocharc.js
require('ts-node/register');Mocha 自身支持浏览器和 Node 端测试,为了在浏览器端测试我们需要写一个 html, 里面使用 <script src="mocha.min.js">的文件,然后再将本地所有文件插入到html中才能完成测试,手动做工程化效率比较低,所以需要借助工具来实现这个任务,这个工具就是 Karma。
Karma 本质上就是在本地启动一个web服务器,然后再启动一个外部浏览器加载一个引导脚本,这个脚本将我们所有的源文件和测试文件加载到浏览器中,最终就会在浏览器端执行我们的测试用例代码。所以使用 Karma + mocha +chai 即可搭建一个完整的浏览器端的单元测试工具链。
npm install karma mocha chai karma-mocha karma-chai --save-dev
npx karma init
// Which testing framework do you want to use: mocha
// Do you want to use Require.js: no
// Do you want capture any browsers automatically: Chrome这里 Karma 初始化时选择了 Mocha 的支持,然后第二个 Require.js 一般为否,除非业务代码中使用了amd类型的包。第三个选用 Chrome 作为测试浏览器。 然后再在代码里单独配置下 chai 。
// karma.conf.js
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
- frameworks: ['mocha'],
+ frameworks: ['mocha', 'chai'],
// list of files / patterns to load in the browser
files: [],Karma 的 frameworks 作用是在全局注入一些依赖,这里的配置就是将 Mocha 和 chai 提供的测试相关工具暴露在全局上供代码里使用。 Karma 只是将我们的文件发送到浏览器去执行,但是根据前文所述我们的代码需要经过 webpack 或 browserify 打包后才能运行在浏览器端。
如果原始代码已经是 CJS了,可以使用 browserify 来支持浏览器端运行,基本零配置,但是往往现实世界比较复杂,我们有 ES6 ,JSX 以及 TypeScript 要处理,所以这里我们使用 webpack 。
下面是 webpack 的配置信息。
npm install karma-webpack@4 webpack@4 @babel/core @babel/preset-env @babel/preset-react babel-loader --save-dev// karma.conf.js
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'chai'],
// list of files / patterns to load in the browser
files: [
+ { pattern: "test/*.test.js", watched: false }
],
preprocessors: {
+ 'test/**/*.js': [ 'webpack']
},
+ webpack: {
+ module: {
+rules: [{
+ test: /.*\.js/,
+ use: 'babel-loader'
+ }]
+ }
+ },// .babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-react"]
}这里我们测试一个React 程序代码如下:
// js/index.js
import React from 'react';
import ReactDOM from 'react-dom';
export function renderToPage(str) {
const container = document.createElement('div');
document.body.appendChild(container);
console.log('there is real browser');
return new Promise(resolve => {
ReactDOM.render(<div>{ str } </div>, container, resolve);
});
}
// test/index.test.js
import { renderToPage } from '../js/index';
describe('renderToPage', () => {
it ('should render to page', async function () {
let content = 'magic string';
await renderToPage(content);
expect(document.documentElement.innerText).to.be.contain(content);
})
})// js/index.js
import React from 'react';
import ReactDOM from 'react-dom';
export function renderToPage(str) {
const container = document.createElement('div');
document.body.appendChild(container);
console.log('there is real browser');
return new Promise(resolve => {
ReactDOM.render(<div>{ str } </div>, container, resolve);
});
}
// test/index.test.js
import { renderToPage } from '../js/index';
describe('renderToPage', () => {
it ('should render to page', async function () {
let content = 'magic string';
await renderToPage(content);
expect(document.documentElement.innerText).to.be.contain(content);
})
})
并且打开了本地浏览器:
可以看到现在已经在真实浏览器中运行测试程序了。
因为图形化的测试对 CI 机器不友好,所以可以选择 puppeteer 代替 Chrome。
再者这些都是很重的包,如果对真实浏览器依赖性不强,可以使用 JSDOM 在 Node 端模拟一个浏览器环境。
稍微总结下工具链:
· 在 Node 环境下测试工具链可以为 : mocha + chai + babel
· 模拟浏览器环境可以为 : mocha + chai + babel + jsdom
· 在真实浏览器环境下测试工具链可以为 : karma + mocha + chai + webpack + babel
一个测试流水线往往需要很多个工具搭配使用,配置起来比较繁琐,还有一些额外的工具例如单元覆盖率(istanbul),函数/时间模拟 (sinon.js)等工具。工具之间的配合有时候不一定能够完美契合,选型费时费力。
jasmine 的出现就稍微缓解了一下这个问题,但也不够完整,jasmine提供一个测试框架,里面包含了 测试流程框架,断言函数,mock工具等测试中会遇到的工具。可以近似地看作 jasmine = mocha + chai + 辅助工具 。
接下来试一试 jasmine 的工作流程。
使用 npx jasmine init初始化之后会在当前目录中生成spec目录, 其中包含一份默认的配置文件:
// ./spec/support/jasmine.json
{
"spec_dir": "spec",
"spec_files": [
"**/*pec.js"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": true
}如果希望加载一些全局的配置可以在 spec/helpers 目录中放一些js文件, 正如配置所言,jasmine 在启动时会去执行 spec/helpers 目录下的所有js文件。
比如我们常常使用 es6语法,就需要增加es6的支持。
新增 spec/helpers/babel.js 写入如下配置即可。
npm install @babel/register @babel/core @babel/preset-env --save-dev// spec/helpers/babel.js
require('babel-register');// .babelrc
{
"presets": ["@babel/preset-env"]
}和 mocha 一样,如果需要 TypeScript 的支持,可以使用如下配置
npm install ts-node typescript --save-dev// spec/helpers/typescript.js
require('ts-node/register');配置文件中的 spec_dir是 jasmine约定的用例文件目录,spec_files规定了用例文件格式为 xxx.spec.js。
有了这份默认配置就可以按照要求写用例,例如:
// ./spec/index.spec.js
import { multiple } from '../index.js';
describe('Multiple', () => {
it ('should be a function', () => {
expect(multiple).toBeInstanceOf(Function);
})
it ('should 7 * 2 = 14', () => {
expect(multiple(7, 2)).toEqual(14);
})
it ('should 7 * -2 = -14', () => {
expect(multiple(7, -2)).toEqual(-14);
})
})jasmine 的断言风格和 chai 很不一样,jasmine 的 API 如下,与 chai 相比少写了很多 . ,而且支持的功能更加清晰,不用考虑如何组合使用的问题,而且下文介绍的 jest 测试框架也是使用这种风格。
nothing()
toBe(expected)
toBeCloseTo(expected, precisionopt)
toBeDefined()
toBeFalse()
toBeFalsy()
toBeGreaterThan(expected)
toBeGreaterThanOrEqual(expected)
toBeInstanceOf(expected)
toBeLessThan(expected)
toBeLessThanOrEqual(expected)
toBeNaN()
toBeNegativeInfinity()
toBeNull()
toBePositiveInfinity()
toBeTrue()
toBeTruthy()
toBeUndefined()
toContain(expected)
toEqual(expected)
toHaveBeenCalled()
toHaveBeenCalledBefore(expected)
toHaveBeenCalledOnceWith()
toHaveBeenCalledTimes(expected)
toHaveBeenCalledWith()
toHaveClass(expected)
toHaveSize(expected)
toMatch(expected)
toThrow(expectedopt)
toThrowError(expectedopt, messageopt)
toThrowMatching(predicate)
withContext(message) → {matchers} 运行 jasmine 即可生成测试报告:
默认的测试报告不是很直观, 如果希望提供类似 Mocha 风格的报告可以安装 jasmine-spec-reporter ,在 spec/helpers 目录中添加一个配置文件, 例如spec/helpers/reporter.js。
const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
jasmine.getEnv().clearReporters(); // remove default reporter logs
jasmine.getEnv().addReporter(new SpecReporter({// add jasmine-spec-reporter
spec: {
displayPending: true
}
}));此时输出的用例报告如下:
如果在 Jasmine 中执行 DOM 级别的测试,就依然需要借助 Karma 或 JSDOM了,具体的配置这里就不再赘述。
总结下 Jasmine 的工具链:
Node 环境下测试 : Jasmine + babel
模拟 JSDOM 测试 : Jasmine + JSDOM + babel
真实浏览器测试 : Karma + Jasmine + webpack + babel
页:
[1]