使用 PhantomCSS 进行视觉回归测试

Avatar of Jon Bellah
Jon Bellah 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

以下是来自 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 的核心是三个关键组件的组合

  • PhantomJSSlimerJS – 无头浏览器。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 对象中的静态内容替换动态内容。

第二种方法是使用名为 Hologrammdcss 的工具,它允许你在 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 听起来不像你的菜,或者你只是对替代方案感兴趣,我建议你看看