聊一聊前端自动化测试(下)
页面测试
对于浏览器里跑的前端代码,做测试要比Node.js模块要麻烦得多。Node.js模块纯js代码,使用V8运行在本地,测试用的各种各样的依赖和工具都能快速的安装,而前端代码不仅仅要测试js,CSS等等,更麻烦的事需要模拟各种各样的浏览器,比较常见的前端代码测试方案有下面几种:
构建一个测试页面,人肉直接到虚拟机上开各种浏览器跑测试页面(比如公司的f2etest)。这个方案的缺点就是不好做代码覆盖率测试,也不好持续化集成,同时人肉工作较多
使用PhantomJS构建一个伪造的浏览器环境跑单元测试,好处是解决了代码覆盖率问题,也可以做持续集成。这个方案的缺点是PhantomJS毕竟是Qt的webkit,并不是真实浏览器环境,PhantomJS也有各种各样兼容性坑
通过Karma调用本机各种浏览器进行测试,好处是可以跨浏览器做测试,也可以测试覆盖率,但持续集成时需要注意只能开PhantomJS做测试,毕竟集成的Linux环境不可能有浏览器。这可以说是目前看到的最好的前端代码测试方式了
这里以gulp为构建工具做测试,后面在React组件测试部分再介绍以webpack为构建工具做测试
叒一个煎蛋的栗子
前端代码依旧是js,一样可以用Mocha+Should.js来做单元测试。打开node_modules下的Mocha和Should.js,你会发现这些优秀的开源工具已经非常贴心的提供了可在浏览器中直接运行的版本:mocha/mocha.js和should/should.min.js,只需要把他们通过script标签引入即可,另外Mocha还需要引入自己的样式mocha/mocha.css
首先看一下我们的前端项目结构:
.
├── gulpfile.js
├── package.json
├── src
│ └── index.js
└── test
├── test.html
└── test.js
比如这里源码src/index.js就是定义一个全局函数:
window.render = function() {
var ctn = document.createElement('div');
ctn.setAttribute('id', 'tmall');
ctn.appendChild(document.createTextNode('天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com'));
document.body.appendChild(ctn);
}
而测试页面test/test.html大致上是这个样子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="../node_modules/mocha/mocha.css"/>
<script src="../node_modules/mocha/mocha.js"></script>
<script src="../node_modules/should/should.js"></script>
</head>
<body>
<div id="mocha"></div>
<script src="../src/index.js"></script>
<script src="test.js"></script>
</body>
</html>
head里引入了测试框架Mocha和断言库Should.js,测试的结果会被显示在<div id="mocha"></div>这个容器里,而test/test.js里则是我们的测试的代码。
前端页面上测试和Node.js上测试没啥太大不同,只是需要指定Mocha使用的UI,并需要手动调用mocha.run():
mocha.ui('bdd');
describe('Welcome to Tmall', function() {
before(function() {
window.render();
});
it('Hello', function() {
document.getElementById('tmall').textContent.should.be.eql('天猫前端招人,有意向的请发送简历至lingyucoder@gmail.com');
});
});
mocha.run();
在浏览器里打开test/test.html页面,就可以看到效果了:
在不同的浏览器里打开这个页面,就可以看到当前浏览器的测试了。这种方式能兼容最多的浏览器,当然要跨机器之前记得把资源上传到一个测试机器都能访问到的地方,比如CDN。
测试页面有了,那么来试试接入PhantomJS吧
使用PhantomJS进行测试
PhantomJS是一个模拟的浏览器,它能执行js,甚至还有webkit渲染引擎,只是没有浏览器的界面上渲染结果罢了。我们可以使用它做很多事情,比如对网页进行截图,写爬虫爬取异步渲染的页面,以及接下来要介绍的——对页面做测试。
当然,这里我们不是直接使用PhantomJS,而是使用mocha-phantomjs来做测试。npm install --save-dev mocha-phantomjs安装完成后,就可以运行命令./node_modules/.bin/mocha-phantomjs ./test/test.html来对上面那个test/test.html的测试了:
单元测试没问题了,接下来就是代码覆盖率测试
覆盖率打点
首先第一步,改写我们的gulpfile.js:
'use strict';
const gulp = require('gulp');
const istanbul = require('gulp-istanbul');
gulp.task('test', function() {
return gulp.src(['src/**/*.js'])
.pipe(istanbul({
coverageVariable: '__coverage__'
}))
.pipe(gulp.dest('build-test'));
});
这里把覆盖率结果保存到__coverage__里面,把打完点的代码放到build-test目录下,比如刚才的src/index.js的代码,在运行gulp test后,会生成build-test/index.js,内容大致是这个样子:
var __cov_WzFiasMcIh_mBvAjOuQiQg = (Function('return this'))();
if (!__cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__) { __cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__ = {}; }
__cov_WzFiasMcIh_mBvAjOuQiQg = __cov_WzFiasMcIh_mBvAjOuQiQg.__coverage__;
if (!(__cov_WzFiasMcIh_mBvAjOuQiQg['/Users/lingyu/gitlab/dev/mui/test-page/src/index.js'])) {
__cov_WzFiasMcIh_mBvAjOuQiQg['/Users/lingyu/gitlab/dev/mui/test-page/src/index.js'] = {"path":"/Users/lingyu/gitlab/dev/mui/test-page/src/index.js","s":{"1":0,"2":0,"3":0,"4":0,"5":0},"b":{},"f":{"1":0},"fnMap":{"1":{"name":"(anonymous_1)","line":1,"loc":{"start":{"line":1,"column":16},"end":{"line":1,"column":27}}}},"statementMap":{"1":{"start":{"line":1,"column":0},"end":{"line":6,"column":1}},"2":{"start":{"line":2,"column":2},"end":{"line":2,"column":42}},"3":{"start":{"line":3,"column":2},"end":{"line":3,"column":34}},"4":{"start":{"line":4,"column":2},"end":{"line":4,"column":85}},"5":{"start":{"line":5,"column":2},"end":{"line":5,"column":33}}},"branchMap":{}};
}
__cov_WzFiasMcIh_mBvAjOuQiQg = __cov_WzFiasMcIh_mBvAjOuQiQg['/Users/lingyu/gitlab/dev/mui/test-page/src/index.js'];
__cov_WzFiasMcIh_mBvAjOuQiQg.s['1']++;window.render=function(){__cov_WzFiasMcIh_mBvAjOuQiQg.f['1']++;__cov_WzFiasMcIh_mBvAjOuQiQg.s['2']++;var ctn=document.createElement('div');__cov_WzFiasMcIh_mBvAjOuQiQg.s['3']++;ctn.setAttribute('id','tmall');__cov_WzFiasMcIh_mBvAjOuQiQg.s['4']++;ctn.appendChild(document.createTextNode('天猫前端招人\uFF0C有意向的请发送简历至lingyucoder@gmail.com'));__cov_WzFiasMcIh_mBvAjOuQiQg.s['5']++;document.body.appendChild(ctn);};
这都什么鬼!不管了,反正运行它就好。把test/test.html里面引入的代码从src/index.js修改为build-test/index.js,保证页面运行时使用的是编译后的代码。
编写钩子
运行数据会存放到变量__coverage__里,但是我们还需要一段钩子代码在单元测试结束后获取这个变量里的内容。把钩子代码放在test/hook.js下,里面内容这样写:
'use strict';
var fs = require('fs');
module.exports = {
afterEnd: function(runner) {
var coverage = runner.page.evaluate(function() {
return window.__coverage__;
});
if (coverage) {
console.log('Writing coverage to coverage/coverage.json');
fs.write('coverage/coverage.json', JSON.stringify(coverage), 'w');
} else {
console.log('No coverage data generated');
}
}
};
这样准备工作工作就大功告成了,执行命令./node_modules/.bin/mocha-phantomjs ./test/test.html --hooks ./test/hook.js,可以看到如下图结果,同时覆盖率结果被写入到coverage/coverage.json 里面了。
生成页面
有了结果覆盖率结果就可以生成覆盖率页面了,首先看看覆盖率概况吧。执行命令./node_modules/.bin/istanbul report --root coverage text-summary,可以看到下图:
还是原来的配方,还是想熟悉的味道。接下来运行./node_modules/.bin/istanbul report --root coverage lcov生成覆盖率页面,执行完后open coverage/lcov-report/index.html,点击进入到src/index.js:
一颗赛艇!这样我们对前端代码就能做覆盖率测试了
接入Karma
Karma是一个测试集成框架,可以方便地以插件的形式集成测试框架、测试环境、覆盖率工具等等。Karma已经有了一套相当完善的插件体系,这里尝试在PhantomJS、Chrome、FireFox下做测试,首先需要使用npm安装一些依赖:
karma:框架本体
karma-mocha:Mocha测试框架
karma-coverage:覆盖率测试
karma-spec-reporter:测试结果输出
karma-phantomjs-launcher:PhantomJS环境
phantomjs-prebuilt: PhantomJS最新版本
karma-chrome-launcher:Chrome环境
karma-firefox-launcher:Firefox环境
安装完成后,就可以开启我们的Karma之旅了。还是之前的那个项目,我们把该清除的清除,只留下源文件和而是文件,并增加一个karma.conf.js 文件:
.
├── karma.conf.js
├── package.json
├── src
│ └── index.js
└── test
└── test.js
karma.conf.js是Karma框架的配置文件,在这个例子里,它大概是这个样子:
'use strict';
module.exports = function(config) {
config.set({
frameworks: ['mocha'],
files: [
'./node_modules/should/should.js',
'src/**/*.js',
'test/**/*.js'
],
preprocessors: {
'src/**/*.js': ['coverage']
},
plugins: ['karma-mocha', 'karma-phantomjs-launcher', 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-coverage', 'karma-spec-reporter'],
browsers: ['PhantomJS', 'Firefox', 'Chrome'],
reporters: ['spec', 'coverage'],
coverageReporter: {
dir: 'coverage',
reporters: [{
type: 'json',
subdir: '.',
file: 'coverage.json',
}, {
type: 'lcov',
subdir: '.'
}, {
type: 'text-summary'
}]
}
});
};
这些配置都是什么意思呢?这里挨个说明一下:
frameworks: 使用的测试框架,这里依旧是我们熟悉又亲切的Mocha
files:测试页面需要加载的资源,上面的test目录下已经没有test.html了,所有需要加载内容都在这里指定,如果是CDN上的资源,直接写URL也可以,不过建议尽可能使用本地资源,这样测试更快而且即使没网也可以测试。这个例子里,第一行载入的是断言库Should.js,第二行是src下的所有代码,第三行载入测试代码
preprocessors:配置预处理器,在上面files载入对应的文件前,如果在这里配置了预处理器,会先对文件做处理,然后载入处理结果。这个例子里,需要对src目录下的所有资源添加覆盖率打点(这一步之前是通过gulp-istanbul来做,现在karma-coverage框架可以很方便的处理,也不需要钩子啥的了)。后面做React组件测试时也会在这里使用webpack
plugins:安装的插件列表
browsers:需要测试的浏览器,这里我们选择了PhantomJS、FireFox、Chrome
reporters:需要生成哪些代码报告
coverageReporter:覆盖率报告要如何生成,这里我们期望生成和之前一样的报告,包括覆盖率页面、lcov.info、coverage.json、以及命令行里的提示
好了,配置完成,来试试吧,运行./node_modules/karma/bin/karma start --single-run,可以看到如下输出:
可以看到,Karma首先会在9876端口开启一个本地服务,然后分别启动PhantomJS、FireFox、Chrome去加载这个页面,收集到测试结果信息之后分别输出,这样跨浏览器测试就解决啦。如果要新增浏览器就安装对应的浏览器插件,然后在browsers里指定一下即可,非常灵活方便。
那如果我的mac电脑上没有IE,又想测IE,怎么办呢?可以直接运行./node_modules/karma/bin/karma start启动本地服务器,然后使用其他机器开对应浏览器直接访问本机的9876端口(当然这个端口是可配置的)即可,同样移动端的测试也可以采用这个方法。这个方案兼顾了前两个方案的优点,弥补了其不足,是目前看到最优秀的前端代码测试方案了
React组件测试
去年React旋风一般席卷全球,当然天猫也在技术上紧跟时代脚步。天猫商家端业务已经全面切入React,形成了React组件体系,几乎所有新业务都采用React开发,而老业务也在不断向React迁移。React大红大紫,这里单独拉出来讲一讲React+webpack的打包方案如何进行测试
这里只聊React Web,不聊React Native
事实上天猫目前并未采用webpack打包,而是Gulp+Babel编译React CommonJS代码成AMD模块使用,这是为了能够在新老业务使用上更加灵活,当然也有部分业务采用webpack打包并上线
叕一个煎蛋的栗子
这里创建一个React组件,目录结构大致这样(这里略过CSS相关部分,只要跑通了,集成CSS像PostCSS、Less都没啥问题):
.
├── demo
├── karma.conf.js
├── package.json
├── src
│ └── index.jsx
├── test
│ └── index_spec.jsx
├── webpack.dev.js
└── webpack.pub.js
React组件源码src/index.jsx大概是这个样子:
import React from 'react';
class Welcome extends React.Component {
constructor() {
super();
}
render() {
return <div>{this.props.content}</div>;
}
}
Welcome.displayName = 'Welcome';
Welcome.propTypes = {
/**
* content of element
*/
content: React.PropTypes.string
};
Welcome.defaultProps = {
content: 'Hello Tmall'
};
module.exports = Welcome;
那么对应的test/index_spec.jsx则大概是这个样子:
import 'should';
import Welcome from '../src/index.jsx';
import ReactDOM from 'react-dom';
import React from 'react';
import TestUtils from 'react-addons-test-utils';
describe('test', function() {
const container = document.createElement('div');
document.body.appendChild(container);
afterEach(() => {
ReactDOM.unmountComponentAtNode(container);
});
it('Hello Tmall', function() {
let cp = ReactDOM.render(<Welcome/>, container);
let welcome = TestUtils.findRenderedComponentWithType(cp, Welcome);
ReactDOM.findDOMNode(welcome).textContent.should.be.eql('Hello Tmall');
});
});
由于是测试React,自然要使用React的TestUtils,这个工具库提供了不少方便查找节点和组件的方法,最重要的是它提供了模拟事件的API,这可以说是UI测试最重要的一个功能。更多关于TestUtils的使用请参考React官网,这里就不扯了...
代码有了,测试用例也有了,接下就差跑起来了。karma.conf.js肯定就和上面不一样了,首先它要多一个插件karma-webpack,因为我们的React组件是需要webpack打包的,不打包的代码压根就没法运行。另外还需要注意代码覆盖率测试也出现了变化。因为现在多了一层Babel编译,Babel编译ES6、ES7源码生成ES5代码后会产生很多polyfill代码,因此如果对build完成之后的代码做覆盖率测试会包含这些polyfill代码,这样测出来的覆盖率显然是不可靠的,这个问题可以通过isparta-loader来解决。React组件的karma.conf.js大概是这个样子:
'use strict';
const path = require('path');
module.exports = function(config) {
config.set({
frameworks: ['mocha'],
files: [
'./node_modules/phantomjs-polyfill/bind-polyfill.js',
'test/**/*_spec.jsx'
],
plugins: ['karma-webpack', 'karma-mocha',, 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-phantomjs-launcher', 'karma-coverage', 'karma-spec-reporter'],
browsers: ['PhantomJS', 'Firefox', 'Chrome'],
preprocessors: {
'test/**/*_spec.jsx': ['webpack']
},
reporters: ['spec', 'coverage'],
coverageReporter: {
dir: 'coverage',
reporters: [{
type: 'json',
subdir: '.',
file: 'coverage.json',
}, {
type: 'lcov',
subdir: '.'
}, {
type: 'text-summary'
}]
},
webpack: {
module: {
loaders: [{
test: /\.jsx?/,
loaders: ['babel']
}],
preLoaders: [{
test: /\.jsx?$/,
include: [path.resolve('src/')],
loader: 'isparta'
}]
}
},
webpackMiddleware: {
noInfo: true
}
});
};
这里相对于之前的karma.conf.js,主要有以下几点区别:
由于webpack的打包功能,我们在测试代码里直接import组件代码,因此不再需要在files里手动引入组件代码
预处理里面需要对每个测试文件都做webpack打包
添加webpack编译相关配置,在编译源码时,需要定义preLoaders,并使用isparta-loader做代码覆盖率打点
添加webpackMiddleware配置,这里noInfo作用是不需要输出webpack编译时那一大串信息
这样配置基本上就完成了,跑一把./node_modules/karma/bin/karma start --single-run:
很好,结果符合预期。open coverage/lcov-report/index.html打开覆盖率页面:
鹅妹子音!!!直接对jsx代码做的覆盖率测试!这样React组件的测试大体上就完工了
小结
前端的代码测试主要难度是如何模拟各种各样的浏览器环境,Karma给我们提供了很好地方式,对于本地有的浏览器能自动打开并测试,本地没有的浏览器则提供直接访问的页面。前端尤其是移动端浏览器种类繁多,很难做到完美,但我们可以通过这种方式实现主流浏览器的覆盖,保证每次上线大多数用户没有问题。
持续集成
测试结果有了,接下来就是把这些测试结果接入到持续集成之中。持续集成是一种非常优秀的多人开发实践,通过代码push触发钩子,实现自动运行编译、测试等工作。接入持续集成后,我们的每一次push代码,每个Merge Request都会生成对应的测试结果,项目的其他成员可以很清楚地了解到新代码是否影响了现有的功能,在接入自动告警后,可以在代码提交阶段就快速发现错误,提升开发迭代效率。
持续集成会在每次集成时提供一个几乎空白的虚拟机器,并拷贝用户提交的代码到机器本地,通过读取用户项目下的持续集成配置,自动化的安装环境和依赖,编译和测试完成后生成报告,在一段时间之后释放虚拟机器资源。
开源的持续集成
开源比较出名的持续集成服务当属Travis,而代码覆盖率则通过Coveralls,只要有GitHub账户,就可以很轻松的接入Travis和Coveralls,在网站上勾选了需要持续集成的项目以后,每次代码push就会触发自动化测试。这两个网站在跑完测试以后,会自动生成测试结果的小图片
Travis会读取项目下的travis.yml文件,一个简单的例子:
language: node_js
node_js:
- "stable"
- "4.0.0"
- "5.0.0"
script: "npm run test"
after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls"
language定义了运行环境的语言,而对应的node_js可以定义需要在哪几个Node.js版本做测试,比如这里的定义,代表着会分别在最新稳定版、4.0.0、5.0.0版本的Node.js环境下做测试
而script则是测试利用的命令,一般情况下,都应该把自己这个项目开发所需要的命令都写在package.json的scripts里面,比如我们的测试方法./node_modules/karma/bin/karma start --single-run就应当这样写到scripts 里:
{
"scripts": {
"test": "./node_modules/karma/bin/karma start --single-run"
}
}
而after_script则是在测试完成之后运行的命令,这里需要上传覆盖率结果到coveralls,只需要安装coveralls库,然后获取lcov.info上传给Coveralls即可
更多配置请参照Travis 官网介绍
这样配置后,每次push的结果都可以上Travis和Coveralls看构建和代码覆盖率结果了
小结
项目接入持续集成在多人开发同一个仓库时候能起到很大的用途,每次push都能自动触发测试,测试没过会发生告警。如果需求采用Issues+Merge Request来管理,每个需求一个Issue+一个分支,开发完成后提交Merge Request,由项目Owner负责合并,项目质量将更有保障
总结
这里只是前端测试相关知识的一小部分,还有非常多的内容可以深入挖掘,而测试也仅仅是前端流程自动化的一部分。在前端技术快速发展的今天,前端项目不再像当年的刀耕火种一般,越来越多的软件工程经验被集成到前端项目中,前端项目正向工程化、流程化、自动化方向高速奔跑。还有更多优秀的提升开发效率、保证开发质量的自动化方案亟待我们挖掘。