以下是来自 Jon Bellah 的客座文章,他是 10up 的前端工程师主管。Jon 联系我们,希望撰写一篇关于视觉回归测试的文章,这是一种 CSS 测试形式(即确保您不会意外破坏您的网站)。我认为该用例特别有趣:重新构建 CSS(转换为 Sass,拆分文件等)并确保在此过程中没有出现回归。在本文中,Jon 将深入探讨所有这些内容,以及一些视觉测试的挑战(例如,更改内容会更改视觉结果)以及巧妙的解决方法。
从新客户那里继承代码库是我在代理机构工作时遇到的最常见、也是最困难的挑战之一。在某些情况下,客户之所以转向新的代理机构,是因为之前的代理机构没有提供高质量的工作。几乎在每种情况下,之前的代理机构都没有按照我的方式做事。
我经常发现自己处于这种情况。并非每个客户都有需求、愿望或预算从头开始重建。
最近,我的团队从新客户那里继承了一个代码库,并负责进行一些清理工作,并快速过渡到构建新功能。在深入研究之后,我们觉得可以改进他们的代码库,并为我们自己铺平一条更轻松的前进道路,方法是将他们的样式表转换为 Sass。
虽然我们当然可以简单地重命名文件并将它们包含在一个预编译的样式表中(无需进行任何清理),但我们认为通过重新构建他们的样式可以获得很多好处。这样做会花费更多前期成本,但我们认为它最终会为他们节省大量的时间和金钱。最重要的是,它将使我们能够以更大的信心更快地进行迭代。
过去,我会认为这样的工作是相当**高风险**的。毕竟,CSS 中的 C 确实代表级联……顺序绝对很重要。重构几个样式表意味着更改事物出现的顺序,这自然会带来很高的破坏风险。
因此,它一直是手动测试(缓慢)或被认为成本过高的事情。
但是,这次我们决定构建一个视觉回归测试套件。
视觉回归测试最近开始流行起来,并且有充分的理由。从最基本的角度来看,它是一系列测试,这些测试贯穿您的站点,拍摄各种组件的屏幕截图,将这些屏幕截图与基线进行比较,并在发生更改时提醒您。
这对于某些人来说可能听起来违反直觉。我们更改 CSS 是因为我们希望事物看起来有所不同。为什么我们希望构建过程每次提交对样式表的更改时都告诉我们我们破坏了一些东西呢?
无论您是在重新构建客户的样式还是只是与团队合作,都很容易对 CSS 进行更改,我们认为这些更改只会影响一个组件,却发现后来我们在完全不同的页面上破坏了该组件。
为了真正理解为什么视觉回归测试可能是有益的,我认为了解是什么使人类不适合这项工作是有帮助的。
人与机器
事实证明,我们人类在发现视觉刺激的变化方面实际上非常糟糕。事实上,我们无法注意到变化的能力已成为一组日益受到研究的生理和心理现象。
我们甚至还以此为基础制作了游戏。你还记得以前那些“找不同”的图片吗?

心理学家热衷于了解许多现实世界的问题,例如这些现象如何影响目击证词或驾驶能力等方面;但他们在研究中发现了很多可以应用于我们网络开发工作的知识。
我认为与本次讨论特别相关的一种现象是变化盲视。
变化盲视
关于变化盲视概念的研究可以追溯到 20 世纪 70 年代。然而,在 1996 年,伊利诺伊大学厄巴纳-香槟分校的两位教授 George McConkie 和 Christopher Currie 进行了一系列研究,这些研究被认为引发了人们对变化盲视现象的极大兴趣。
变化盲视是一种感知缺陷,在这种缺陷中,视觉刺激的变化可以在观察者没有注意到它们的情况下发生。它与任何视觉缺陷无关,纯粹是心理上的。
在 McConkie & Currie 的研究中,他们发现,在某些情况下,高达五分之一的图片区域的变化经常会被忽视。 此视频 提供了一个极好的例子,说明如果您不寻找变化,有多少变化会被错过。
工具
在构建测试套件时,您可以选择各种工具。我始终建议四处查看,比较工具,并找出最适合您的工具。
考虑到这一点,我选择 PhantomCSS 作为我的视觉回归测试的首选工具。我选择它有几个原因。
首先,因为它在 GitHub 上拥有一个相对活跃且健康的社区。对于开源软件,我始终喜欢检查并确保某个工具或库仍在积极开发中。依赖于废弃软件很快就会变成一个痛苦的过程。
我选择 PhantomCSS 的第二个原因是它有一个方便的 Grunt 插件,它可以轻松地与我现有的工作流程集成。
PhantomCSS 的核心是三个关键组件的组合
- PhantomJS 或 SlimerJS – 无头浏览器。PhantomJS 是 WebKit 的无头版本,而 Slimer 是 Firefox 使用的 Gecko 引擎。
- CasperJS – Casper 是一个 JavaScript 导航脚本和测试实用程序。它允许我们在无头浏览器中定义一组要执行的操作。
- ResembleJS – Resemble 是一个用于进行图像比较的 JavaScript/HTML5 库。它会将我们的新测试与我们的基线进行测试,并提醒我们两个测试之间的任何差异。
最后,如前所述,我们将使用 Grunt 运行我们的测试。
实现
现在我们知道了是什么和为什么,让我们逐步了解如何设置和实现您的视觉回归测试套件。
设置 Grunt
首先,我们需要设置 Grunt 来运行我们的测试套件,因此您需要确保已安装 Grunt。
在命令行中,$ cd /path/to/your-site
并运行
$ npm install @micahgodbolt/grunt-phantomcss --save-dev
打开项目的 `Gruntfile` 并加载 PhantomCSS 任务,并在 `grunt.initConfig()` 中定义该任务,如下所示
grunt.loadNpmTasks('@micahgodbolt/grunt-phantomcss');
grunt.initConfig({
phantomcss: {
desktop: {
options: {
screenshots: 'baselines/desktop',
results: 'results/desktop',
viewportSize: [1280, 800]
},
src: [
'tests/phantomcss/start.js',
'tests/phantomcss/*-test.js'
]
}
}
});
测试不同的断点
我喜欢使用 Sass MQ 来管理我的断点。这种方法的额外好处是它可以为我提供所有断点的列表,我可以轻松地使用这些列表来设置我的测试。
使用 PhantomCSS,您可以在实际的测试定义中操作浏览器宽度,但我更喜欢将它从我的测试中抽象出来,以便为我的视觉测试套件提供更大的灵活性。相反,选择在 Grunt 任务中定义它。
使用 grunt-phantomcss,我们可以定义一组要在不同断点运行的测试,并且作为额外奖励,将它们保存到不同的文件夹中。
为了使事情更有条理和语义化,我还将每个测试子任务命名与其对应的 Sass MQ 断点相匹配。
例如
grunt.initConfig( {
pkg: grunt.file.readJSON('package.json'),
phantomcss: {
desktop: {
options: {
screenshots: 'baselines/desktop',
results: 'results/desktop',
viewportSize: [1024, 768]
},
src: [
'tests/phantomcss/start.js',
'tests/phantomcss/*-test.js'
]
},
mobile: {
options: {
screenshots: 'baselines/mobile',
results: 'results/mobile',
viewportSize: [320, 480]
},
src: [
'tests/phantomcss/start.js',
'test/phantomcss/*-test.js'
]
}
}
});
在这里,我们正在运行同一组测试,但它们在不同的断点运行,并保存到我们基线和结果中的子文件夹中。
设置测试套件
在我们的 Grunt 定义中,你可以看到我们首先运行 `tests/phantomcss/start.js`。这个文件启动了 Casper(然后启动我们的脚本工具和无头浏览器),并且应该看起来像这样
phantom.casperTest = true;
casper.start();
现在,回到我们的 Grunt 定义中,你可以看到我们接下来运行 `tests/phantomcss/` 目录下所有以 `-test.js` 结尾的文件。Grunt 会按照字母顺序循环遍历每个文件。
如何组织你的测试文件完全取决于你。我个人喜欢为网站中的每个组件创建一个测试文件。
编写你的第一个测试
一旦你设置了 `start.js` 文件,就可以编写你的第一个测试了。我们将这个文件命名为 `header-test.js`。
casper.thenOpen('http://mysite.dev/')
.then(function() {
phantomcss.screenshot('.site-header', 'site-header');
});
在文件顶部,我们告诉 Casper 打开根 URL,然后在我们的第一个测试中,我们获取整个 ` .site-header` 元素的屏幕截图。第二个参数是我们屏幕截图文件的名称。我更喜欢根据元素或组件命名屏幕截图,因为它使我的测试套件更具语义化,并且更容易与团队成员共享。
最简单的形式,这就是你需要为第一个测试编写的全部内容。但是,我们可以构建一个更强大的测试套件,涵盖更多实际的元素、页面和应用程序状态。
编写交互
Casper 允许我们自动化无头浏览器中发生的交互。例如,如果我们想测试按钮的悬停状态,我们可以这样写
casper.then(function() {
this.mouse.move('.button');
phantomcss.screenshot('.button');
});
你还可以测试登录和注销状态。在我们的 `start.js` 文件中,我们可以编写一个小函数,在我们启动 Casper 实例后立即填写 WordPress 登录表单。
casper.start('http://default.wordpress.dev/wp-admin/', function() {
this.fill('form#loginform', {
'log': 'admin',
'pwd': 'password'
}, true);
this.click('#wp-submit');
console.log('Logging in...');
});
你会注意到,我们是在 `casper.start()` 上运行它,而不是在它自己的单独测试中运行它。在 `start.js` 文件中的 `casper.start()` 上设置你的会话,使得会话可用于测试套件中的其他文件,因为它总是先运行。
我建议查看 Casper 文档 以获取有关编写交互的更多信息。
运行测试
现在我们已经构建了一个基本的测试套件,是时候运行我们的测试了。在命令行中,运行 `$ grunt phantomcss`。
PhantomCSS 会自动将第一次运行的屏幕截图设置为基线,以便与所有将来的测试进行比较。

如果你的测试失败了,例如上面的那个,PhantomCSS 会将三个不同的屏幕截图输出到你的结果文件夹中。它会输出原始截图、一个 `.diff.png` 和一个 `.fail.png`。
例如,我们更改了文章页面中文本的字体大小,但无意中减小了归档视图中的字体大小。PhantomCSS 会给我们这些差异进行比较

挑战
构建视觉回归测试套件当然并非没有挑战。我遇到的两个最大挑战是动态内容和在团队中分配测试。
动态内容
第一个,并且在某种程度上是最困难的挑战,是我遇到的如何处理动态内容。测试套件正在遍历这些页面,拍摄屏幕截图并进行比较。如果内容不同,测试就会失败。
如果你与团队合作,很可能每个人都会针对他们自己的本地环境进行测试。针对单个登台环境进行测试并不总是能解决问题,因为内容仍然可能在那里发生变化;例如,一组随机排序的相关帖子。
为了解决这个问题,我采用了两种方法。
第一种,也是我最喜欢的一种方法,是使用 Javascript 将你正在测试的元素中的内容替换为一组代表性的演示内容。
由于这些测试不应部署到你的生产服务器,因此你不必担心 XSS 漏洞。因此,我喜欢在我的测试中使用 `.html()`,在拍摄屏幕截图之前,用我包含在代码库中的 JSON 对象中的静态内容替换动态内容。
第二种方法是使用名为 Hologram 或 mdcss 的工具,它允许你在 CSS 中使用注释来创建自动生成的样式指南。这种方法需要更大的开销,因为它需要工作流程发生最大的变化,但它额外的好处是为你的前端组件创建了优秀的文档。
分发
我遇到的第二个主要挑战是在确定如何在工程师团队中最好地分配这些测试。到目前为止,在我们的测试中,我们已经对测试 URL 进行了硬编码,这在与团队合作时会导致问题,因为每个人可能不会对他们的本地环境使用相同的 URL。
为了解决这个问题,我和我的团队将我们的 `$ grunt test` 任务注册为接受 `--url` 参数,然后使用 grunt.log 将其保存到本地文件。
// All a variable to be passed, eg. --url=http://test.dev
var localURL = grunt.option( 'url' );
/**
* Register a custom task to save the local URL, which is then read by the PhantomCSS test file.
* This file is saved so that "grunt test" can then be run in the future without passing your local URL each time.
*
* Note: Make sure test/visual/.local_url is added to your .gitignore
*
* Props to Zack Rothauser for this approach.
*/
grunt.registerTask('test', 'Runs PhantomCSS and stores the --url parameter', function() {
if (localURL) {
grunt.log.writeln( 'Local URL: ' + localURL );
grunt.file.write( 'test/visual/.local_url', localURL );
}
grunt.task.run(['phantomcss']);
});
然后,在测试文件的顶部,你将使用
var fs = require('fs'), siteURL;
try {
siteURL = fs.read( 'test/visual/.local_url' );
} catch(err) {
siteURL = (typeof siteURL === 'undefined') ? 'http://local.wordpress.dev' : siteURL;
}
casper.thenOpen(siteURL + '/path/to/template');
你的套件现在会在每次运行时查找 `.local_url` 文件,但如果文件不存在,它将默认为使用 `http://local.wordpress.dev`。
结束语
视觉回归测试可以为你的项目带来许多好处。快速迭代和持续集成日益成为当今开发人员的口号,构建一个安全网是很有意义的。
视觉回归测试套件对于与开源项目人员合作也很有用。事实上,WordPress 项目正在努力 创建一个模式库,并附带一个回归测试套件。这个测试套件将为 WordPress 项目提供基础,以便他们能够继续推进恢复其样式表正常状态的计划。
替代方案
PhantomCSS 不是唯一可用的工具,它只是我认为适合我、我的团队和我们工作流程的工具。如果视觉回归测试听起来很酷,但 PhantomCSS 听起来不像你的菜,或者你只是对替代方案感兴趣,我建议你看看
我之前研究过这个,这是一个非常酷的想法。不幸的是,我没有时间在我的当前项目中实现它 :(
我也很失望地发现你没有向我们展示并排图像的结果。我想知道我是否找到了所有结果。
哈哈,抱歉没有并排图像的差异!也许可以将其通过 PhantomCSS 运行 :)
在我拥有一个允许我实施完整测试套件的预算的项目之前,花了一些时间。希望你可以在你的下一个项目中尝试一下!
这里有一个快速比较:http://codepen.io/agop/full/yYwBLb
(来自 这里 的技术,也在 CSS-Tricks 上看到过。)
很有趣的文章。我目前正在尝试使用BackstopJS。感觉还可以吧。设置起来相当容易,但还有很多不足之处。Backstop在最新版本中刚刚实现了CasperJS自动化,但有点令人失望,因为所有脚本都先运行,然后才截取屏幕截图。我想我会尝试使用PhantomCSS,因为它可能更容易编写针对菜单等需要多个截图的测试用例。
谢谢!我也在使用Backstop时遇到了问题,这就是为什么我没有在替代方案中列出的原因。希望PhantomCSS能满足你的需求!
我现在也在评估工具,并且对BackstopJS的第一印象不错(包括Pat评论中提到的易于设置,以及不错的报告)。
除了脚本的时序问题之外,您是否知道BackstopJS还有其他问题?
【精彩的文章——谢谢John!】
您提到在您的方法中,每个人都在本地运行测试。
我只是想知道您如何解决跨平台渲染差异?例如,相同的标记在Mac上的PhantomJS和Windows上的PhantomJS中呈现效果不同。
您的所有团队成员都使用相同的操作系统类型,或者您是否对测试使用了一些不匹配容差?
最近我们实现了类似的系统,但我们没有在本地环境中进行测试(由于跨平台渲染问题导致意外错误),而是使用同一组机器(平台+设备+浏览器版本,通常是Sauce Labs)。
这是一个非常好的观点……实际上也是我在推荐中包含Selenium的确切原因。Selenium允许您实际测试不同的平台等。如果您需要更全面的覆盖范围,我认为它是一个很好的替代方案。
我认为视觉回归套件并不是QA流程的完全替代品,我认为它只是该流程的一个很好的补充。我仍然使用BrowserStack和虚拟机来测试跨浏览器和跨平台。
我们并不都使用相同的操作系统,我们使用了一些不匹配容差,这似乎解决了大部分问题。
PhantomCSS是一个不错的工具,但它对我们的团队来说效果不佳。我们在CI集成方面遇到了困难,并且仅使用phantomJS的方法并不是理想的情况,尤其是在企业环境中。最终,我们的团队切换到Gemini,结果证明它功能强大得多。它还不算流行,但值得一试。在我看来,Gemini是目前“视觉回归”领域最好的工具。
啊!我真不敢相信我忘记在我的列表中包含Gemini。我对它的经验不多,但它是我最初查看的工具之一。
感谢提醒!
感谢这篇文章!
您是否查看过Gemini CSS回归测试工具?它比PhantomCSS好得多。
其主要功能包括
– 与不同的浏览器兼容,而不仅仅是PhantomJS
– 计算元素的位置和大小,包括其box-shadow和outline属性
– 忽略图像之间的一些特殊情况差异(渲染伪影、文本光标等)。
– 以及杀手级功能之一:CSS代码覆盖率统计
https://github.com/gemini-testing/gemini/
它还有一个名为Gemini GUI的配套实用程序,可以极大地帮助在代码更改后更新基线图像。
https://github.com/gemini-testing/gemini-gui/
Gemini还可以使用插件进行扩展。
感谢您深思熟虑的回复!同意Gemini是一个很棒的工具……完全忘记在我的替代推荐中包含它了。
谢谢Jon,很棒的文章!!!
查看Dave Haeffner的这篇精彩文章 – http://bit.ly/1DCS61Y – 它解释了如何开始使用视觉回归测试,并介绍了一些领先的工具(包括如何处理视觉回归测试中的误报)。
谢谢!已将该链接添加为书签,我会在有空的时候查看。
很高兴看到如何自己构建它,但如果您想要一个简单的付费选项,那么SpeedCurve也会根据WebPageTest的屏幕截图进行视觉差异比较。由于它是WebPageTest,您还可以在一系列真实的浏览器中进行测试。
一些示例:https://speedcurve.com/blog/visual-diffs-on-every-deploy/
最好在文章末尾添加一个付费选项列表。
(免责声明:我是SpeedCurve的创始人)
非常感谢您的分享!
我在测试使用httpS的网站时遇到问题,它会失败SSL握手,是否有任何选项可以传递到casper配置(在start.js内部)?因为所有解决方案都与–ignore-ssl-errors=true和–ssl-protocol=any有关。
提前感谢!
不适用于flexbox :(
嗨,Michael,您需要升级PhantomJS和CasperJS才能支持flexbox。我碰巧在项目中遇到了同样的问题,所以知道这个技巧。尝试使用PhantomCSS 2.0.0。
对我来说,另一个很大的挑战是Linux和OSX之间PhantomJS渲染的差异。我在CI服务器上设置了测试,但我需要维护两组不同的基线屏幕截图,因为OSX上的phantomjs渲染页面与Ubuntu上的渲染方式不同(字体略有不同,边距等)。