本文作者:IMWeb yangchunwen 原文出处:IMWeb社区 未经同意,禁止转载
首先要解释一下为什么叫浏览器自动化测试,因为本文只关注发布后页面功能的自动化测试,也就是UI层面的自动化。
浏览器测试有别于js代码的单元测试,后者一般是发布前的代码功能逻辑测试,在这方面已经有很多比较成熟的方案,如jasmine mocha Qunit...
个人认为自动化测试的主要出发点有两点:
说到浏览器自动化测试,不得不介绍大名鼎鼎的phantomjs及casperjs。phantomjs可以理解为一个无界面的浏览器,可以通过流水线式的代码来驱动其页面的浏览行为,而后者是前者在易用性API上的一些封装。
这里演示下使用casperjs截取百度首页
关于这两个东西的安装,有兴趣体验的建议去看官方文档,其实很简单,这里不一一赘述。
首先创建一个js文件baidu.js:
var casper = require('casper').create();
casper.start();
casper.thenOpen('http://www.baidu.com/', function () {
casper.captureSelector('baidu.png', 'html');
});
casper.run();
以上代码主要做了三件事:
require('casper').create()
,可以理解为一个浏览器进程casper.thenOpen(...)
;casper.captureSelector
在命令行运行
casperjs baidu.js
看看此脚本生成的图片结果
等等!为什么这个图只有400X300的大小?
原因是我创建了一个浏览器进程去加载页面,但是没有指明用什么浏览器去加载。所以在创建casper实例的时候,可以指定浏览器的窗口大小,甚至我们可以通过指定userAgent的方式冒充手机端的浏览器。例如我们将其指定为iPhone5的safari,并设置窗口大小:
var casper = require('casper').create({
pageSettings: {
// 冒充浏览器
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53'
},
// 浏览器窗口大小
viewportSize: {
width: 320,
height: 568
}
});
再次运行后我们的是这样,是不是手机浏览时的样子了?
以上的例子,可以知道了怎么使用一个无界面的浏览器去加载页面,并获得页面的界面截图。
我们可以不打开浏览器,一行命令就可以知道页面长啥样了,所以每次我们只要运行这个casperjs脚本,通过截图就能看到我们页面是不是正常的。
但是,通过肉眼去判断,肯定是有违“自动化”的初衷的,所以必须要借助工具来帮我们分析。
最简单直观的办法就是“像素对比”,也就是把两次或多次的截图,逐一对比每一像素或一定范围区域,这样就能产出图片的差别了,如下图:
实际应用中,可以指定一个图片作为基准图,每一次我们截取的页面图与之对比,如果不一样,就可以说明线上的页面出现了异常。
像素对比这样的工具已经比较成熟,这里介绍一个与前端开发非常亲近的方案:Resemble.js。
为什么说它和前端亲近,因为它使用的是canvas。我们知道,每一个图片的每一像素,都可以通过RBGA(red,blue,green,alpha)三个值来确定:
Resemble.js的主要原理就是把我们需要对比的图片绘制到canvas中,读取需要对比的像素点(或区域)的image data,从而确定图片的差异。
为了与phantomjs/casperjs更好的结合,Resemble.js作者同时做了基于Resemble.js的封装phantomcss。
phantomcss使用了简单的API来做图片对比:
phantomcss.screenshot( "#CSS .selector1", screenshotName1);
phantomcss.screenshot( "#CSS .selector2", screenshotName2);
phantomcss.compareFiles(screenshotName1, screenshotName2);
假如对比的图片有不一致的地方,会生成一张对比图,同时有差异的地方会用显眼的颜色标出,类似这样:
页面截图对比出现不一致,并不能证明我们的页面就出现了异常,例如广告位等,这些变化频繁的区域,每一次对比都有可能出现差异,所以对广告位或其他经常变化的位置不宜所差异对比。 实际应用中,对整个页面进行截图对比是不推荐的,这样的方式过于简单粗暴,我们更应该对页面的各个区域进行细分对比,做细粒度的监控。
既然是浏览器测试,不能没有cookie的参与,casperjs没有对cookie的操作作封装,可以使用phantomjs直接“种”cookie:
phantom.addCookie({
name: 'cookie',
value: 'value',
domain: '.xx.com',
path: '/',
secure: false,
httponly: false,
expires: Date.now() + (1000 * 60 * 60 * 24 * 5)
});
在前面的开胃菜中,我们访问到的页面都是没有登录态的,这里通过手动植入百度帐号登录态的cookie值来实现登录访问。
在PC端chrome中打开百度首页,并用你的帐号登录,在开发者工具中复制百度帐号关键cookie BDUSS的值
并hard code到你的casperjs脚本中:
phantom.addCookie({
name: 'BDUSS',
value: '你复制的cookie值',
domain: '.baidu.com',
path: '/',
secure: false,
httponly: false,
expires: Date.now() + (1000 * 60 * 60 * 24 * 5)
});
Done!右上角已经有用户名,说明此时我们已经登录了!
简单的截图+对比还远远达不到我们的测试要求,对于自动化原则来说,为我们实现自动化的页面交互才是王道,别急,这就来。
前面介绍了手动种植cookie的方式实现登录,下面看下怎么实现手机端百度的登录过程。
先预览下整个脚本login.js的代码,下面解释一下整个过程:
loadImages: false
casper.captureSelector('1.png', 'html');
这一步会得到图片1.png,并且右上角是没有用户名的(未登录):
var cookies = phantom.cookies;
for (var i = 0, len = cookies.length; i < len; i++) {
console.log(cookies[i].name + ': ' + cookies[i].value);
}
这一步输出到命令行的结果:
可以看到,当前的cookie中还没有百度账号的关键cookie BDUSS。
casper.mouse.click('#login');
casperjs(phantomjs)支持了非常丰富的可以支持复杂交互的鼠标事件:
鼠标事件支持指定操作目标的CSS3路径
casper.wait(3000);
casper.captureSelector('2.png', 'html');
casper.evaluate(function () {
document.querySelector('[name=username]').value = '***';
document.querySelector('[name=password]').value = '***';
});
这里使用了一个非常有用的方法evaluate。
最后,运行测试脚本casperjs login.js
能得到4张截图,分别记录了整个登录交互过程中关键步骤的交互效果:
对比图1和图4,区别在于图4右上角的用户名:
同时,在命令行中最后还读取到了登录后的BDUSS cookie值:
phantomjs(casperjs)不仅可以在当前页面操作,还可以把当前context切换到iframe里进行操作,这点给嵌入iframe的页面测试带来了很多方便。
phantomjs(casperjs)支持使用CSS3选择器及XPath的方式对我们需要操作的目标进行操作(点击、截图等),还可以通过指定区域边界的来操作,例如可以指定x/y坐标/width/height来进行点击或截图等:
casper.capture('capture.png', {
x: 200,
y: 300,
width: 200,
height: 300
});
现实世界的web里,有些交互功能不是机械的操作,有时候出于安全或其他因素考虑,页面会做一些限制,要求我们的交互需要根据一些动态输出,这种功能是很难做到完全自动化的,例如,上面的百度登录功能,有时候会出现验证码的情况:
这时候就很难借助机器来帮我们做登录了,所以在前面我要介绍通过手动植入cookie的方式实现登录。
通过前面的介绍,使用phantomjs(casperjs)已经能实现很多自动化的功能,在此基础上,实现单元测试就很简单了。
casperjs提供了相对比较完善的单元测试API
单元测试中,每一个testsuite都被包装在一个闭包中:
casper.test.begin('your testsuite', 0, function (test) {
// 单元测试代码
});
例如下面是一个测试百度页面title及log位置是否正确的一组测试用例:
casper.test.begin('test demo', 0, function (test) {
casper.start();
casper.thenOpen('http://www.baidu.com/', function () {
// 测试页面title是否正确
casper.test.assertTitle('百度一下', 'page title');
// 测试logo的位置信息
// 获取x/y/width/height
var layout = casper.getElementBounds('#logo');
layout = JSON.stringify(layout);
// 验证是否符合
casper.test.assertEquals(layout, '{"height":87,"left":0,"top":53,"width":320}', 'logo\'s boundary');
});
casper.run(function () {
casper.test.done();
});
});
完整代码test.js在这里
运行casperjs test test.js
可以在命令行中看到以下输出:
两个case都已pass!
与前面的截图肉眼查看的方式相比,单元测试为我们提供了更加简洁的测试结果。
另外,casperjs的test模块还可以在测试后产出XML结果,例如上面那个例子的结果如下:
<?xml version="1.0" encoding="UTF-8" ?>
<testsuites time="2.228">
<testsuite name="test demo" tests="2" failures="0" errors="0" time="2.228"
timestamp="2015-08-31T13:30:12.462Z" package="test">
<testcase name="page title" classname="test" time="2.223">
</testcase>
<testcase name="logo's boundary" classname="test" time="0.005">
</testcase>
<system-out>
</system-out>
</testsuite>
</testsuites>
利用这个XML结果,与报警等系统结合,可以实现各种强大的自动化功能。