随着互联网第二春的到来以及 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
什么是视觉感知测试 -Perceptual Testing
对于界面布局,传统的测试都是由人工对比设计图和产品界面。当界面有修改之后,再由人通过肉眼去检查修改 (包括正确的和错误的修改),这样即费时而且测试结果又不稳定,因为人是有情绪的。但是我们认为如果一个界面通过第一次的人工验证并发布之后,它就是一个正确的标准界面,并且是包含了人工测试价值的资产。当下一次测试的时候,这部分价值就应该被保留并重用起来,用于减少新的一次测试的时间,从而实现界面的快速回归测试。
为了解决上面提到的各种问题,视觉感知测试孕育而生。它使用传统的对图片进行二进制比较的办法,结合敏捷迭代开发的理念,产生的一种针对界面布局的自动化测试方法。
视觉感知测试包含以下几个主要的测试步骤:
- 对于产品版本进行截图(产品线上环境或者类产品环境) 首先人工完成第一个软件版本的测试并部署上线,在第二个版本需要进行测试的时候首先对第一个版本的所有界面进行截图。
- 对于新的发行版进行截图(比如 staging 环境) 然后对第二个需要进行测试的版本的所有界面也进行截图。
- 配对 URL(忽略 hostname) 通过配对 URL,对所有的截图按照相同的 URL 进行分组。当然有时候会出现新的界面,有时候老的界面会被删除。对于新的界面就需要人工进行首次验证测试 。
- 像素级别的图形比较 对于分组之后的截图进行像素级别的比较并生产差别图。有时候为了降噪,可以只对局部关心的组件进行比较。
- 人工查看所有不同 最后通过人工审查差别图报告完成测试。
视觉感知测试的一些实例
1,CSS 的改变
对于开发人员,CSS 改变之后的 side effect 是最头痛的事情,下面展示了当 CSS 改版之后页面的变化
。
图 4,CSS 的改变
2,内容的改变
对于测试人员,大量复杂页面的微小修改很难发现的,下面展示了如果使用视觉比较找到差异。
图 5,内容的改变 1
图 6,内容的改变 2
3,事件处理的改变
对于测试人员,有些界面需要鼠标点击或者悬停才能展现出来。而对于这样的界面的测试就必须人工来做。下面展示了一个选择框在鼠标点击之后在两个版本之间产生的差异。
图 7,事件处理的改变
4,响应式设计
对于开发和测试人员,如果要测试响应式设计就必须使用不同分辨率的设备,模拟器或者调整浏览器到各种分辨率,这将是一个费时费力费钱的工作。下面展示了如果视觉比较如果检查响应式设计。
图 8,响应式设计
视觉对比 - UI 自动化 end-to-end 测试的最后一公里
持续交付中的视觉感知测试
下图为传统的持续交付流程:
图 9,没有视觉感知的持续交付
下图为加入了视觉感知测试的持续交付流程,其中主要的区别就是部署之前要并行与其他自动化测试做一次视觉感知测试。
图 10,包含视觉感知测试的持续交付
下图为实施了视觉感知测试之后对于界面回归测试的时间示意图
图 11,界面回归测试时间示意图
三个视觉感知测试工具
1,Mogotest
- 来自 Mogotest,基于“云”
- Restful API 开发测试
- 支持多浏览器
- 不支持本地化
- 商用
- http://mogotest.com/
Mogotest 是一个商用的产品,它提供一个“云”测试平台,可以让用户在其平台上使用各种不同的浏览器访问被测试页面,并进行对比。主要目的是测试不同浏览器之间的兼容性,不能测试动态页面等。
2,DPXDT
- 来自 Google, 基于 Python
- 默认只支持 PhantomJS(Headless)
- Restful API 开发测试
- 支持人工认证报表系统
- 开源,免费
- https://github.com/bslatkin/dpxdt
Dpxdt 是基于 Python 和 PhantomJS 开发的一个 Web Service 系统,其中 PhantomJS 可以理解为一个没有界面的浏览器。用户使用其提供的 RESTFul API 可以十分方便的对比两个页面,而且它还提供一个功能十分强大的报表系统。对于全部是静态页面的 Web 系统来说非常适用,不过对于需要手动导航,比如需要进行输入,点击或者鼠标悬停等操作之后才能进行检测的界面,它默认并不支持,需要对其本身进行修改才可以。不过它还提供了一个方式可以把他很方便的部署到 GWS 上。
3,Viff
- 来自 ThoughtWorks,基于 Javascript/node
- 支持多浏览器(Selenium WebDriver)
- JavaScript/DSL 开发测试
- 支持人工认证报表系统
- 开源,免费
- 支持嵌入式测试:智能电视,手机等
- https://github.com/winsonwq/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/
VIFF 演示
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); }); }); <span color="#00ff00">describe('in build environment', function() {</span> beforeEach(function(done) { browser .get("http://localhost:8000/example/build/github.html") .waitForElementByCssSelector('.repo-list-item', browser.isDisplayed()) .nodeify(done); }); <span color="#0000ff">it('could go to Home Page', function(done) {</span> browser.title().should.become("TJ Holowaychuk's Github Repositories") .then(function () { <span color="#ff0000">buildScreenshot({ 'Home Page': ['/github.html'] }, 'screenshots/homepage.png', done);</span> }); }); <span color="#0000ff">it('should filter repo as per keyword', function(done) {</span> browser .elementByCssSelector('[type="search"]').type('commander.js') .sleep(1000) .elementsByCssSelector('.repo-list-item') .then(function (elements) { elements.length.should.eql(1); <span color="#ff0000"> buildScreenshot({ 'Search Result': ['/github.html'] }, 'screenshots/filter.png', done);</span> }); }); <span color="#0000ff">it('should open readme file', function(done) {</span> browser .elementByCssSelector('.repo-list-item:nth-child(2)').click() .waitForElementByCssSelector('.repo-readme', browser.isDisplayed()) .elementByCssSelector('.repo-readme').text().should.eventually.contain("# Co") .then(function () { <span color="#ff0000">buildScreenshot({ 'Should open readme file': ['/github.html'] }, 'screenshots/github.png', done);</span> }); }); }); <span color="#00ff00">describe('in prod environment', function() { </span> beforeEach(function(done) { browser .get("http://localhost:8000/example/prod/github.html") .waitForElementByCssSelector('.repo-list-item', browser.isDisplayed()) .nodeify(done); }); <span color="#0000ff">it('could go to Home Page', function(done) {</span> browser.title().should.become("TJ Holowaychuk's Github Repositories").then(function () { <span color="#ff0000">prodScreenshot({ 'Home Page': ['/github.html'] }, 'screenshots/homepage.png', done); </span><span color="#000000"> });</span> }); <span color="#0000ff"> it('should filter repo as per keyword', function(done) {</span> browser.elementByCssSelector('[type="search"]').type('commander.js') .sleep(1000) .elementsByCssSelector('.repo-list-item') .then(function (elements) { elements.length.should.eql(1); <span color="#ff0000">prodScreenshot({ 'Search Result': ['/github.html'] }, 'screenshots/filter.png', done);</span> }); }); <span color="#0000ff">it('should open readme file', function(done) {</span> browser .elementByCssSelector('.repo-list-item:nth-child(2)').click() .waitForElementByCssSelector('.repo-readme', browser.isDisplayed()) .elementByCssSelector('.repo-readme').text().should.eventually.contain("# Co") .then(function () { <span color="#ff0000">prodScreenshot({ 'Should open readme file': ['/github.html'] }, 'screenshots/github.png', done); </span> }); }); }); }); 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)
视觉感知测试的几个主要使用场景
- Web 应用界面的回归测试
- 智能嵌入式应用界面的回归测试 - 如智能电视应用,智能手机应用
- 大数据呈现的回归测试
总结
视觉感知测试是近几年为了解决大量繁重的人工界面回归测试才出现的一种自动化测试方法。它不仅能帮助测试人员进行界面回归测试,而且还能帮助开发人员在重构或修改公共 UI 代码的时候快速进行 side effect 检查,从而大大减少了测试的时间,并且使得对大量的界面进行回归测试成为了现实,最终增加了软件的质量,特别是最大可能的保证了软件的用户体验。希望在不久的将来,越来越多的复杂界面系统将会使用视觉感知测试来快速完成其全面的回归测试,从而实现真正的高质量的快速持续交付。
附 1:
图 15,大家来找茬结果
图 16,网页对比结果
作者简介
刘冉,现任 ThoughtWorks 高级软件质量咨询师, 超过 10 年软件开发和测试工作工作经验。最熟悉的领域是嵌入式系统开发、Linux 系统开发、各种脚本、各种测试工具、各种自动化测试系统开发、以及 Agile 中的 QA。其中对于服务器性能测试,Web 功能测试,以及测试分层一体化解决方案有较深的理解。现在关注于全方位自动化 QA 的工作,以及对于 Agile 流程中怎么实现统一的流程、故事、功能、测试和文档管理,以及质量控制度量。
感谢张逸对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论