随着互联网第二春的到来以及Web2.0的盛行,Web应用程序开发已经成为了当前软件开发的主力军。现在无论是企业级应用,社交应用还是移动应用,Web已经成为标准配置,而且很多企业正在逐步的将自己的企业级本地应用进行互联网Web化。但是Web 的界面布局测试,多浏览器测试,CSS/JavsScript的重构等都成为界面测试的痛中之痛,特别是大型Web应用的回归测试量太大,从而导致回归测试很多时候根本无法完成,所以很少会有团队能完成全方位的界面布局回归测试,特别是对于使用Agile流程开发的团队就更加困难。
而且现在大家对用户体验以及持续部署越来越重视,导致Web应用程序的界面开发和测试难上加难。
首先来看看Web 界面开发和测试为什么如此困难。
1,Web界面布局回归测试
对于Web网页界面布局测试一般都是由人工手动对比设计图和产品界面。而人工对比测试存在两个问题:a,速度慢;b,人的不确定性。对于拥有大量复杂界面的企业级Web应用,界面布局的回归测试的数量巨大,再加上这两个问题,导致这类应用的界面布局回归测试时间很长,成本很高,所以很多基于Agile项目基本不可能在迭代周期内高质量的完成其界面回归测试。对于每天做一次回归,那更是不可能完成的任务。
(下面有一个游戏“大家来找茬”,请读者用心找找有多少处不同,并记录一下用了多少时间。答案在附1图中)
图1,大家来找茬
2,CSS/JavaScript代码重构
现在Web前端越来越复杂,所以代码量也急速增加,导致前端开发像后端开发一样开始使用基于Library, Module和Pattern的开发方式。从而产生了一个问题:当有公共代码被修改和重构之后,如何快速发现界面的side effect?
由于CSS和控制界面的JavaScript代码被重构之后,只能通过人眼手动检测其正确性,导致开发和测试人员很难在有限的时间找到所有被修改的代码影响到的界面进行检查。最后很可能会有一些side effect在开发和测试阶段都不被发现而进入产品环境。
3,多浏览器
Web应用其最大的优势就是其可以跨平台跨浏览器,使用者可以在不同的操作系统中使用不同的浏览器访问并使用Web应用。但是这个优势也带来了很大的问题:需要做大量的浏览器兼容性测试。而被测浏览器的数量越多(现在的主流浏览器包括IE, Chrome, Firefox, Safari等,并且每种浏览器还有很多种版本),测试数量和时间也会成倍增长。这个痛也导致很多大型Web应用基本上很难在限定时间内完成大部分主流浏览器的兼容性测试。如果一定要做,那么也需要付出巨大的成本,比如添加更多的测试人员。
4,响应式设计(Responsive Web Design)测试
由于移动设备的普及,导致大量的用户使用手机或者平板使用Web应用。由于移动设备拥有各种各样的分辨率,因此设计人员也开始考虑针对不同的分辨率设计应用界面,响应式设计(Responsive Web Design)也孕育而生。但是响应式设计很难测试,基本上只能靠手工进行,而且还需要准备各种分辨率的设备或者各种分辨率的浏览器。需要测试的分辨率越多,测试的时间越长,成本就也越高。
下面有两张真实网页的截图,其中有很多不同之处,读者可以尝试再找一下有多少。答案在附2图中。
图2,网页1
图3,网页2
对于界面布局,传统的测试都是由人工对比设计图和产品界面。当界面有修改之后,再由人通过肉眼去检查修改(包括正确的和错误的修改),这样即费时而且测试结果又不稳定,因为人是有情绪的。但是我们认为如果一个界面通过第一次的人工验证并发布之后,它就是一个正确的标准界面,并且是包含了人工测试价值的资产。当下一次测试的时候,这部分价值就应该被保留并重用起来,用于减少新的一次测试的时间,从而实现界面的快速回归测试。
为了解决上面提到的各种问题,视觉感知测试孕育而生。它使用传统的对图片进行二进制比较的办法,结合敏捷迭代开发的理念,产生的一种针对界面布局的自动化测试方法。
视觉感知测试包含以下几个主要的测试步骤:
首先人工完成第一个软件版本的测试并部署上线,在第二个版本需要进行测试的时候首先对第一个版本的所有界面进行截图。
然后对第二个需要进行测试的版本的所有界面也进行截图。
通过配对URL,对所有的截图按照相同的URL进行分组。当然有时候会出现新的界面,有时候老的界面会被删除。对于新的界面就需要人工进行首次验证测试 。
对于分组之后的截图进行像素级别的比较并生产差别图。有时候为了降噪,可以只对局部关心的组件进行比较。
最后通过人工审查差别图报告完成测试。
1,CSS的改变
对于开发人员,CSS改变之后的side effect是最头痛的事情,下面展示了当CSS改版之后页面的变化
。
图4,CSS的改变
2,内容的改变
对于测试人员,大量复杂页面的微小修改很难发现的,下面展示了如果使用视觉比较找到差异。
图5,内容的改变1
图6,内容的改变2
3,事件处理的改变
对于测试人员,有些界面需要鼠标点击或者悬停才能展现出来。而对于这样的界面的测试就必须人工来做。下面展示了一个选择框在鼠标点击之后在两个版本之间产生的差异。
图7,事件处理的改变
4,响应式设计
对于开发和测试人员,如果要测试响应式设计就必须使用不同分辨率的设备,模拟器或者调整浏览器到各种分辨率,这将是一个费时费力费钱的工作。下面展示了如果视觉比较如果检查响应式设计。
图8,响应式设计
下图为传统的持续交付流程:
图9,没有视觉感知的持续交付
下图为加入了视觉感知测试的持续交付流程,其中主要的区别就是部署之前要并行与其他自动化测试做一次视觉感知测试。
图10,包含视觉感知测试的持续交付
下图为实施了视觉感知测试之后对于界面回归测试的时间示意图
图11,界面回归测试时间示意图
1,Mogotest
Mogotest是一个商用的产品,它提供一个“云”测试平台,可以让用户在其平台上使用各种不同的浏览器访问被测试页面,并进行对比。主要目的是测试不同浏览器之间的兼容性,不能测试动态页面等。
2,DPXDT
Dpxdt是基于Python和PhantomJS开发的一个Web Service系统,其中PhantomJS可以理解为一个没有界面的浏览器。用户使用其提供的RESTFul API可以十分方便的对比两个页面,而且它还提供一个功能十分强大的报表系统。对于全部是静态页面的Web系统来说非常适用,不过对于需要手动导航,比如需要进行输入,点击或者鼠标悬停等操作之后才能进行检测的界面,它默认并不支持,需要对其本身进行修改才可以。不过它还提供了一个方式可以把他很方便的部署到GWS上。
3,Viff
Viff是基于NodeJS和Selenium开发的一个本地工具。通过编写JavaScript代码来调用Selenium API, 并在真实的浏览器中进行截图比较。所以它比较适合动态的Web系统,因为可以编写代码模拟用户输入和点击操作。由于它底层使用的是Selenium作为驱动,所以他支持多种浏览器,比如IE,Chrome,Firefox等。由于最新的Selenium加入了对Android和iOS的支持,因而Viff也能够支持Android和iOS上的浏览器测试。
如果对你来说搭建多浏览器环境比较困难,比如需要同时测试IE8,IE9,IE10等,可以选择BrowserStack。BrowserStack是一个商业产品,他同时通过Web界面和API接口提供多浏览器环境给客户进行Web测试,Viff可以使用其API进行进行多浏览器截图。对于Viff,由于编写JavaScript代码也需要一定的门槛,所以对于没有代码能力的使用者在测试静态网页的时候应该选择Dpxdt,但是如果你有一定的代码能力,并且希望能在当前的功能测试里面加上视觉感知测试或者希望对局部的界面进行测试,建议选用Viff。现在Viff正在开发Web Service功能,这样以后就可以作为一个Service进行部署和使用。
还有其他的视觉感知测试工具,这里就不一一熬述了。在VIFF的官网上有一张多个工具的比较图,有兴趣的读者可以参考一下:http://twers.github.io/Viff-Service/
1,安装
VIFF的安装步骤请参考其项目上的说明文档https://github.com/winsonwq/viff。
为了帮助大家理解和学习VIFF,我们还开发了一系列的Examples和Demos,请参见https://github.com/winsonwq/viff-examples,以下所有代码全部来自这个项目。
2,安装需要被测试的演示网站
下载https://github.com/winsonwq/viff-examples上的代码,模拟产品版本的站点在viff-examples/example/prod里面,模拟需要测试的站点在viff-examples/example/build里面。由于演示网站都是静态代码,所以用任意一个HTTP Sever进行部署都可以,比如我使用Nginx将其部署在本地的8000端口上。部署成功之后,通过http://localhost:8000/example/build 和http://localhost:8000/example/prod 就可以访问到两个测试演示站点。
3,使用VIFF的Main API进行测试
对于一个全新的项目,直接使用VIFF的Main API编写测试代码,如下:
'use strict' var config = module.exports = { seleniumHost: 'http://localhost:4444/wd/hub', browsers: ['firefox'], envHosts: { build: 'http://localhost:8000/example/build', prod: 'http://localhost:8000/example/prod' }, paths: [], reportFormat: 'file', test: function test (description, caseConfig) { var c = {}; c[description] = caseConfig; this.paths.push(c); } }; config.test('Home Page', ['/github.html', function (browser) { return browser.waitForElementByCssSelector('.repo-list-item', browser.isDisplayed()); }]); config.test('Search Result', ['/github.html', function (browser) { return browser .waitForElementByCssSelector('.repo-list-item', browser.isDisplayed()) .elementByCssSelector('[type="search"]').type('commander.js') .sleep(1000); }]); config.test('Open Readme file', ['/github.html', function (browser) { return browser .waitForElementByCssSelector('.repo-list-item', browser.isDisplayed()) .elementByCssSelector('.repo-list-item:nth-child(2)').click() .waitForElementByCssSelector('.repo-readme', browser.isDisplayed()); }])
测试结果报表如下:
1,首先测试主页,由于没有任何改动,所以测试结果是绿色,表示产品环境和测试环境没有任何改变。
图12,演示结果报表1
2,然后在搜索输入框中输入commander.js,结果发现产品版本上的show 1 repository 在测试版本上变成了showing 1 repositories,所以测试结果是红色。然后通过报表可以在立即发现改变,然后在进行人工审核其修改的正确性。这里明显是测试版本出现了错误,然后针对这个错误就可以上报一个bug了。
图13,演示结果报表2
3,清空输入框,然后在主页中点击co,然后测试结果还是红色。认真仔细的查看才发现generator在测试版本中被改成了generators。在如此多的内容中找到一个字母s的改变是非常困难的,但是通过VIFF的测试报告,又一次快速的轻松地发现了改变。
图14,演示结果报表3
4,使用VIFF的Client API进行测试
对于一个已经有Functional Testing的项目,可以不需要重新开发VIFF测试代码,只需要在功能测试代码中调用其Client API,同样可以完成视觉感知测试。由于当前VIFF只开发了JavaScript版本的Client API,所以下面的例子使用的是基于JavaScript开发的Functional Testing。以后VIFF会初步提供基于其他语言的Client API,比如Java,Python 和 Ruby等。
(代码说明,其中两行绿色代码表示分别对产品环境和测试环境进行Functional Testing;其中所有的蓝色代码是Functional Testing的代码;其中所有红色部分代码是VIFF的Client API)
var wd = require('wd'); var chai = require("chai"); var chaiAsPromised = require("chai-as-promised"); chai.use(chaiAsPromised); chai.should(); var ViffClient = require('viff-client'); chaiAsPromised.transferPromiseness = wd.transferPromiseness; describe('Github Page Test', function() { this.timeout(100000); var browser, buildScreenshot, prodScreenshot, buildClient, prodClient; before(function (done) { browser = wd.promiseChainRemote('http://localhost:4444/wd/hub'); browser.init({ browserName: 'firefox' }).nodeify(done); buildClient = new ViffClient('http://localhost:3000', { name: 'build', host: 'http://localhost:8000/example/build/github.html', capabilities: 'firefox' }); buildScreenshot = prepareTakeScreenshot(browser, buildClient); prodClient = new ViffClient('http://localhost:3000', { name: 'prod', host: 'http://localhost:8000/example/prod/github.html', capabilities: 'firefox' }); prodScreenshot = prepareTakeScreenshot(browser, prodClient); }); after(function (done) { buildClient.generateReport(function () { browser.quit().nodeify(done); }); }); describe('in build environment', function() { beforeEach(function(done) { browser .get("http://localhost:8000/example/build/github.html") .waitForElementByCssSelector('.repo-list-item', browser.isDisplayed()) .nodeify(done); }); it('could go to Home Page', function(done) { browser.title().should.become("TJ Holowaychuk's Github Repositories") .then(function () { buildScreenshot({ 'Home Page': ['/github.html'] }, 'screenshots/homepage.png', done); }); }); it('should filter repo as per keyword', function(done) { browser .elementByCssSelector('[type="search"]').type('commander.js') .sleep(1000) .elementsByCssSelector('.repo-list-item') .then(function (elements) { elements.length.should.eql(1); buildScreenshot({ 'Search Result': ['/github.html'] }, 'screenshots/filter.png', done); }); }); it('should open readme file', function(done) { browser .elementByCssSelector('.repo-list-item:nth-child(2)').click() .waitForElementByCssSelector('.repo-readme', browser.isDisplayed()) .elementByCssSelector('.repo-readme').text().should.eventually.contain("# Co") .then(function () { buildScreenshot({ 'Should open readme file': ['/github.html'] }, 'screenshots/github.png', done); }); }); }); describe('in prod environment', function() { beforeEach(function(done) { browser .get("http://localhost:8000/example/prod/github.html") .waitForElementByCssSelector('.repo-list-item', browser.isDisplayed()) .nodeify(done); }); it('could go to Home Page', function(done) { browser.title().should.become("TJ Holowaychuk's Github Repositories").then(function () { prodScreenshot({ 'Home Page': ['/github.html'] }, 'screenshots/homepage.png', done); }); }); it('should filter repo as per keyword', function(done) { browser.elementByCssSelector('[type="search"]').type('commander.js') .sleep(1000) .elementsByCssSelector('.repo-list-item') .then(function (elements) { elements.length.should.eql(1); prodScreenshot({ 'Search Result': ['/github.html'] }, 'screenshots/filter.png', done); }); }); it('should open readme file', function(done) { browser .elementByCssSelector('.repo-list-item:nth-child(2)').click() .waitForElementByCssSelector('.repo-readme', browser.isDisplayed()) .elementByCssSelector('.repo-readme').text().should.eventually.contain("# Co") .then(function () { prodScreenshot({ 'Should open readme file': ['/github.html'] }, 'screenshots/github.png', done); }); }); }); }); function prepareTakeScreenshot(browser, viffClient) { return function (url, imagePath, callback) { browser .saveScreenshot(imagePath) .then(function () { viffClient.post(url, imagePath, callback); }); }; }
测试结果报表同第2步一样,见图12,图13和图14
5,使用VIFF对移动Web进行测试
由于VIFF使用的Selenium,而Selenium本身支持移动Web支持,所以VIFF与生俱来就包含此功能。比如现在要对iPhone进行测试,只需要将第2步Demo代码中的firefox改成iPhone就可以了。(如果对Selenium测试iPhone的测试环境有疑问,请参见Selenium官方文档和Selenium iPhoneDriver: https://code.google.com/p/selenium/wiki/IPhoneDriver)
视觉感知测试是近几年为了解决大量繁重的人工界面回归测试才出现的一种自动化测试方法。它不仅能帮助测试人员进行界面回归测试,而且还能帮助开发人员在重构或修改公共UI代码的时候快速进行side effect检查,从而大大减少了测试的时间,并且使得对大量的界面进行回归测试成为了现实,最终增加了软件的质量,特别是最大可能的保证了软件的用户体验。希望在不久的将来,越来越多的复杂界面系统将会使用视觉感知测试来快速完成其全面的回归测试,从而实现真正的高质量的快速持续交付。
附1:
图15,大家来找茬结果
图16,网页对比结果
刘冉,现任ThoughtWorks高级软件质量咨询师, 超过10年软件开发和测试工作工作经验。最熟悉的领域是嵌入式系统开发、Linux系统开发、各种脚本、各种测试工具、各种自动化测试系统开发、以及Agile中的QA。其中对于服务器性能测试,Web功能测试,以及测试分层一体化解决方案有较深的理解。现在关注于全方位自动化QA的工作,以及对于Agile流程中怎么实现统一的流程、故事、功能、测试和文档管理,以及质量控制度量。