之前一直想尝试前端的自动化测试,一直不知道怎么去入手。众所周知,前端的页面尤其是面向普通用户的页面的话,从用户交互的角度来说,ui的操作占据了绝大部分,ui的这部分的测试(点击,切换等等操作)很难来实现自动化的测试,之前查过一些资料,尝试写过jasmine、karma、selenium之类的工具的demo,当时觉得,一是测试用例不好写,二是很多东西是ui的自动化测试,这种测试版实现起来太麻烦,所以就丢到一边了。这里要吐槽一下自己,前端其实有很多发展的方向,都很不错,很多时候自己都是简单尝试一下,也没有深入的去研究,很多时候可能还是一个兴趣的问题吧。
回到自动化测试上来说,为什么最近我又重新想起了这个东西。我工作有一年多了,基本上就是在开发维护两个项目,一个项目基本上是维护,我来的时候,那个项目已经比较成熟了,另一个项目是我入职后才开始的,所以很多代码都是我写的,但是当时项目做得比较急,而且当时比较水,我们组也没有code review的这个步骤,所以就导致了代码的质量差了点。代码差体现在以下几点:3.代码存在大量的冗余。这个有三个原因,一个是某个模块自己本身的代码冗余,这是功力不到的原因,二是某些地方修改之后,为了向后兼容,增加了一些冗余的代码,三是某些需求的修改,比如说原来网站要求支持到IE6,但是后来就变成IE7了,所以之前写过的兼容IE6的代码都是冗余的代码了。
前一段时间,有一个需求,正好会动到我上边提到的文件比较大的那个页,是可以模块化的那一部分,然后我就把那一块摘出来了。摘出来其实是一个比较大的改动,怎么能确保这一个改动不会影响原来的功能呢,只能靠自己的代码的质量来保证,即使是这样子,我改完了之后还是得测一下可能会受影响的功能和页面。又过了一段时间,线上突然间出了一个bug,某个功能不能用了(感谢测试),我一查,之前某个上线的改动影响到这个页了,测试并不清楚这个改动会影响这个页,所以没有人发现。再回到我之前写的代码,早晚我需要优化掉他们,所以我又想起了自动化测试,这样子可以尽量保证优化不影响原有的功能。
简单查了一下,可能业界的前端自动化测试主要是分两个方向,一个是单元的测试,再一个就是ui的测试了。我们的项目单元测试也是耦合了大量的ui操作,当然也可以把我们现在的模块拆成两层,分的更细,让最基础的那一层不依赖于ui,我觉得这个的成本太高,所以就直接研究ui的自动化测试。先看一下我们要使用到的工具,首先是phantomjs,phantomjs是一个无头浏览器,简单来说就是一个没有实际“显示”的浏览器,虽然你看不到他的显示效果,但是他确实是一个浏览器,很多人用它来写爬虫,我们用它来做最基础的服务。怎么安装就不提了,我是win7,下载下来之后,配置一下环境变量就ok了。
第二个要用到的是casperjs,这个是针对phantomjs的一个封装,包括click、表单填充的功能都封了一层。我初步是这么想的,casperjs的操作是支持链式调用的,通过casperjs.then()来调用,类似于mocha或者jasmine这种断言库,是支持嵌套的,那么这两个组合起来的话,会比较灵活,比如说类似于CMD这样的做法,一个页有一个入口,入口往下是一层一层的测试用例,每一个测试用例代表了一组或者一个操作,单个操作就使用casperjs.then来组合。这是我初步的想法,因为还没有开始真正的写,不知道实际写出来是什么样子的,这个会发后续的文章。casperjs可以通过npm来安装,安装之后也可以配置环境变量来进行全局调用。mocha-casperjs
这个操作会自动寻找并执行调用路径下面的test文件夹(类似于mocha)。这一步我遇到了第一个问题,提示can't open c://progrom,我们来看一下mocha-casperjs的执行文件,/bin/mocha-casperjs.bat 最后一行casperjs %MOCHA_CASPER_PATH%\cli.js --mocha-casperjs-path=%MOCHA_CASPER_PATH%..%*
可以看到这个入口只是对casperjs调用的一个封装,实际是capsperjs 执行/bin/cli.js ,然后问题来了我安装mocha-casperjs的路径是c://progrom files/...,实际执行的命令就变成了casperjs c://progrom files/xxx/bin/cli.js。请注意progrom files中间的那个空格,我最开始认为是casperjs对路径解析的问题,遇到这种空格错误的解析,后来我发现实际是这种命令行的工具都是这样子的,空格隔开的是参数,所以解析出来的话,c://progrom是路径的参数,所以就会出现can't open c://progrom的错误。解决方法很简单,.bat文件在调用的路径外面套一层引号,把完整路径包起来,这样子就不会出错了。这个问题以后也可能会遇到,所以在这里提一下。if (typeof chai === 'undefined') {
console.log('This example requires chai to be installed adjacent to mocha-casperjs')
casper.exit(-1)
}
describe('baidu searching', function() {
before(function() {
casper.start('http://www.baidu.com/')
})
it('should load success', function() {
casper.then(function() {
casper.getTitle().should.contain('百度')
casper.exists('form[action="/s"]').should.be.true
this.fill('form[action="/s"]', {
wd: 'casperjs'
}, true)
})
casper.waitForUrl(/wd=casperjs/, function() {
casper.getTitle().should.match(/casperjs/)
})
})
})
调用mocha-casperjs.bat,结果不太符合我们的预期。phantom.outputEncoding="GBK";//设定输出编码
这一行的作用就是设定phantomjs的输出编码格式变为gbk的。 fmt = color('bright pass', ' ')
+ color('green', ' %d passing')
+ color('light', ' (%s)');
console.log(fmt,
stats.passes || 0,
ms(stats.duration));
很明显,是先装配出了输出的格式,然后直接console输出。我们看一下,color这个函数的实现,/**
* Color `str` with the given `type`,
* allowing colors to be disabled,
* as well as user-defined color
* schemes.
*
* @param {string} type
* @param {string} str
* @return {string}
* @api private
*/
var color = exports.color = function(type, str) {
if (!exports.useColors) {
return String(str);
}
return '\u001b[' + exports.colors[type] + 'm' + str + '\u001b[0m';
};
抛开一些其他的逻辑,实际上就是用‘\u001b[’之类的特殊字符进行的包装,那么这些字符的作用是什么,我们可以看一下这一篇文章,利用正则实现彩色控制台输出,大体上讲的话,就是这些是ansi中的转义字符,语法是\x1b[nm,通过变化n的值来实现不同的颜色。mocha是先定义了一些常用的颜色表,然后在不同的情况下,用不同的颜色包裹起内容,来实现不同颜色的输出。 // Mocha needs the formating feature of console.log so copy node's format function and
// monkey-patch it into place. This code is copied from node's, links copyright applies.
// https://github.com/joyent/node/blob/master/lib/util.js
console.format = function (f) {
...
};
var origError = console.error,
origLog = console.log
console.log = function() { origLog.call(console, console.format.apply(console, arguments)) }
这里面提到mocha需要使用console的format输出,因此作者采用了node的util模块的format函数来实现这部分功能,然后在下面重载了console.log函数,那我们这个问题就有解决思路了,重载console.log,拿到原来要输出的内容,然后对某些字符串加上颜色包裹,然后正常输出,应该可以实现我们的功能。/mocha-casperjs增加如下代码: console.log = function(){
var args = colorHandle(arguments);//颜色处理
origLog.call(console, console.format.apply(console, args));
}
var colors = {//对应的颜色列表
'pass': 90
, 'fail': 31
, 'bright pass': 92
, 'bright fail': 91
, 'bright yellow': 93
, 'pending': 36
, 'suite': 0
, 'error title': 0
, 'error message': 31
, 'error stack': 90
, 'checkmark': 32
, 'fast': 90
, 'medium': 33
, 'slow': 31
, 'green': 32
, 'light': 90
, 'diff gutter': 90
, 'diff added': 42
, 'diff removed': 41
};
var colorsMap = {'passing':'green','failing':'fail'};//不同的状态对应的color值
var colorHandle = function(arguments){//对不同的状态增加不同的显示颜色
var item, args = Array.prototype.slice.call(arguments);
for(var i=0, length = args.length; i=0){
args[i] = colorHandle_(colorsMap[key], args[i]);//增加颜色
}
}
}
}
return args;
};
var colorHandle_ = function(type, str){
return '\u001b[' + colors[type] + 'm' + str + '\u001b[0m';
};
这里采用就是针对含有关键字的字符串做包裹,实现的比较糙,但是这个思路走下去的话可以对输出做更多个性化的定制,我们来看一下最后的效果。好啦,这是我的第三篇博客,之前说要一直更的,但是懒啊。上次backbone写完之后,还尝试过react.js,觉得挺好用的,也没有记录一下,看以后有没有机会补上,不过我看悬,哈哈,继续努力啦。