用Cypress进行可视化回归测试

679 阅读7分钟

用Cypress进行可视化回归测试

每当我们的组件库Picasso的新版本发布时,我们就会更新我们所有的前端应用程序,以获得新功能的最大效益,并使我们的设计在网站的所有部分保持一致。

上个月,我们为Toptal人才门户推出了Picasso更新,这是我们的人才用来寻找工作和与客户互动的平台。由于知道这个版本会有重大的设计变化,并且为了尽量减少意外问题,使用视觉回归测试技术来帮助我们在发布前发现问题是有意义的。

视觉回归测试并不是一个新的概念;Toptal的许多其他项目已经在使用它,包括Picasso本身

像Percy、Happo和Chromatic这样的工具可以用来帮助团队建立一个健康的视觉回归管道,我们一开始确实考虑过加入它们。我们最终决定,设置过程太耗时,而且可能会破坏我们的计划。我们已经为代码冻结设定了一个日期,以便开始迁移,而离最后期限只剩下几天了,我们别无选择,只能发挥创意。

通过UI测试进行可视化回归测试

虽然我们在项目中没有视觉回归测试,但我们确实使用Cypress对UI集成测试进行了良好的覆盖。尽管这不是该工具的主要用途,但Cypress在其文档中有一页专门用于可视化测试,另一页列出了所有可用的插件,以帮助配置Cypress进行可视化测试。

从Cypress到截图

在浏览了可用的文档后,我们决定给cypress-snapshot-plugin一个尝试。它只花了几分钟就完成了设置,一旦完成,我们很快就意识到我们不是在追求传统的可视化回归输出。

大多数视觉回归工具通过比较快照和检测已知的、可接受的基线与页面或组件的修改版本之间的像素差异来帮助识别不需要的变化。如果像素差异大于设定的容忍阈值,该页面或组件将被标记为手动检查。不过,在这个版本中,我们知道我们将对大多数UI组件进行一些小的修改,所以设置一个阈值并不适用。即使一个给定的组件碰巧是100%的不同,它在新版本的背景下可能仍然是正确的。同样地,小到几个像素的偏差也可能意味着一个组件目前不适合生产。

在这一点上,有两件截然不同的事情变得很清楚:注意到像素差异并不能帮助识别问题,而对组件进行并排比较正是我们需要的。我们把快照插件放在一边,并着手创建一组应用Picasso更新之前和之后的组件的图像。这样一来,我们就可以快速扫描所有的变化,以确定新版本是否仍然符合网站的需求和图书馆的标准。

新的计划是对一个组件进行截图,将其存储在本地,在有更新的Picasso版本的分支中对同一组件进行新的截图,然后将它们合并成一张图片。最终,这种新方法与我们开始时的方法没有太大区别,但它在实施阶段给了我们更多的灵活性,因为我们不再需要导入插件和使用它的新命令。

利用API进行图像比较

有了明确的目标,是时候看看Cypress如何帮助我们获得我们需要的截图了。如前所述,我们有大量的UI测试,涵盖了大部分的人才门户网站,所以为了尽可能多地收集关键组件,我们决定在每次互动后对单个元素进行截图。

另一种方法是在测试的关键时刻对整个页面进行截图,但我们认为这些图片太难比较了。另外,这样的比较可能更容易出现人为错误,例如错过了页脚的变化。

第三种选择是通过每一个测试案例来决定捕捉什么,但这将花费更多的时间,所以坚持在页面上使用的所有元素似乎是一个实际的妥协。

我们求助于Cypress的API来生成图片。cy.screenshot() 命令可以开箱即用,创建单个组件的图像,而After Screenshot API允许我们重命名文件,改变目录,并将视觉回归运行与标准运行区分开来。通过结合这两者,我们创建了不影响功能测试的运行,并使我们能够将图像存储在适当的文件夹中。

首先,我们扩展了插件目录下的index.js 文件,以支持两种新的运行类型(基线和比较)。然后,我们根据运行类型来设置图像的路径。

// plugins/index.js
const fs = require('fs')
const path = require('path')
module.exports = (on, config) => {
// Adding these values to your config object allows you to access them in your tests.
  config.env.baseline = process.env.BASELINE || false
  config.env.comparison = process.env.COMPARISON || false

  on('after:screenshot', details => {
    // We only want to modify the behavior of baseline and comparison runs.
    if (config.env.baseline || config.env.comparison) {
      // We keep track of the file name and number to make sure they are saved in the proper order and in their relevant folders.
      // An alternative would have been to look up the folder for the latest image, but this was the simpler approach.
      let lastScreenshotFile = ''
      let lastScreenshotNumber = 0

      // We append the proper suffix number to the image, create the folder, and move the file.
      const createDirAndRename = filePath => {
        if (lastScreenshotFile === filePath) {
          lastScreenshotNumber++
        } else {
          lastScreenshotNumber = 0
        }
        lastScreenshotFile = filePath
        const newPath = filePath.replace(
          '.png',
          ` #${lastScreenshotNumber}.png`
        )

        return new Promise((resolve, reject) => {
          fs.mkdir(path.dirname(newPath), { recursive: true }, mkdirErr => {
            if (mkdirErr) {
              return reject(mkdirErr)
            }
            fs.rename(details.path, newPath, renameErr => {
              if (renameErr) {
                return reject(renameErr)
              }
              resolve({ path: newPath })
            })
          })
        })
      }

      const screenshotPath = `visualComparison/${config.env.baseline ? 'baseline' : 'comparison'}`

      return createDirAndRename(details.path
        .replace('cypress/integration', screenshotPath)
        .replace('All Specs', screenshotPath)
      )
    }
  })
  return config
}

然后,我们通过在项目的package.json 中的Cypress调用中添加相应的环境变量来调用每个运行。

"scripts": {
  "cypress:baseline": "BASELINE=true yarn cypress:open",
  "cypress:comparison": "COMPARISON=true yarn cypress:open"
}

一旦我们运行了我们的新命令,我们可以看到在运行过程中拍摄的所有屏幕截图都被移到了适当的文件夹里。

接下来,我们试图覆盖cy.get() ,Cypress的主要命令是返回DOM元素,并对任何元素的调用及其默认实现进行截图。不幸的是,cy.get() 是一个棘手的命令,因为在它自己的定义中调用原始命令会导致一个无限的循环。建议解决这个限制的方法是创建一个单独的自定义命令,然后让这个新的命令在找到元素后进行截图。

Cypress.Commands.add("getAndScreenshot", (selector, options) => {
  // Note: You might need to tweak the command when getting multiple elements.
  return cy.get(selector).screenshot()
});

it("get overwrite", () => {
  cy.visit("https://example.cypress.io/commands/actions");
  cy.getAndScreenshot(".action-email")
})

然而,我们与页面上的元素互动的调用已经被包裹在一个内部的getElement() 函数中了。因此,我们所要做的就是确保在调用封装器的时候进行截图。

通过视觉回归测试获得的结果

一旦我们有了屏幕截图,唯一要做的就是合并它们。为此,我们用Canvas创建了一个简单的节点脚本。最后,该脚本使我们能够生成618张对比图片有些差异很容易通过打开人才门户网站发现,但有些问题并不那么明显。

图4.不遵循新的毕加索准则的例子;预计有差异,但新版本应该有红色背景和白色文字

图5.组件布局稍有破损的例子

为UI测试增加价值

首先,增加的视觉回归测试被证明是有用的,发现了一些我们在没有这些测试的情况下可能错过的问题。即使我们预期我们的组件会有差异,但了解实际的变化有助于缩小有问题的案例。所以,如果你的项目有一个界面,但你还没有执行这些测试,那就赶紧去做吧

这里的第二个教训,也许是更重要的一个,是我们再次被提醒,完美是美好的敌人。如果我们因为事先没有设置而排除了对这个版本进行视觉回归测试的可能性,我们可能会在迁移过程中错过一些错误。相反,我们商定了一个计划,虽然不是很理想,但可以快速执行,我们朝着这个方向努力,并且得到了回报。