在我的上
一篇文章中,我谈到了使用
Gemini引擎开发视觉测试或视觉回归测试的经验。 这样的测试通过将当前屏幕截图与以前固定的参考屏幕截图进行比较,来检查下一次更改后UI中是否有“移动”的内容。 从那时起,我们编写视觉测试的方法发生了很大变化,包括所使用的引擎。 现在我们使用
Hermione ,但是在本文中,我不仅要讲述Hermione,而且要讲述的是自那时以来积累的问题以及如何解决这些问题,其中包括导致向新引擎过渡的问题。
首先,尽管测试有效且非常成功,但我们对测试涵盖的内容和未涵盖的内容并不清楚。 当然,对覆盖程度有一些了解,但我们没有对其进行定量测量。 其次,测试的组成随着时间的推移而增加,并且不同的测试经常测试同一件事,因为 在不同的屏幕截图中,某些部分与同一部分重合,但是在不同的屏幕截图中。 结果,即使CSS的微小更改也可能一次淹没许多测试,并且需要更新大量标准。 第三,我们的产品中出现了深色主题,为了以某种方式覆盖测试,某些测试被选择性地切换为使用深色主题,这也没有为确定覆盖程度增加问题的清晰度。
性能优化
奇怪的是,我们从优化性能开始。 我将解释原因。 我们的视觉测试基于
故事书 。 故事书中的每个故事都不是单个组件,而是一个完整的“块”(例如,带有实体列表,实体卡,对话甚至整个应用程序的网格)。 要显示此块,有必要使用数据“泵送”故事,不仅是向用户显示的数据,还包括数据块内使用的组件的状态。 此信息与源代码一起以json文件的形式存储,其中包含应用程序状态(redux存储)状态的序列化表示。 是的,从某种程度上来说,这些数据是多余的,但是它极大地简化了测试的创建。 要创建新测试,我们只需在应用程序中打开所需的卡,列表或对话框,对应用程序的当前状态进行快照并将其序列化为文件。 然后,我们添加一个新故事,并进行测试以获取该故事的屏幕截图(全部在几行代码中)。
这种方法不可避免地增加了捆束的尺寸。 数据中的重复程度只是“翻转”。 运行测试时,双子星引擎会在单独的浏览器会话中执行每个测试套件。 每个会话都会重新加载捆绑软件,并且这种方案中捆绑软件的大小与最后一个值相差甚远。
为了减少测试运行时间,我们通过增加测试套件的数量来减少测试套件的数量。 因此,一个测试套件可能会同时影响多个故事。 在此方案中,由于Gemini允许您仅针对整个测试套件设置屏幕截图区域(实际上API允许您在每个屏幕截图之前执行此操作,但实际上它不起作用),我们实际上失去了仅对屏幕的特定区域进行``屏幕显示''的能力。
在测试中无法限制屏幕截图的面积导致参考图像中的视觉信息重复。 尽管没有太多测试,但是这个问题似乎并不重要。 是的,UI不会经常更改。 但这不可能永远持续下去-即将出现的重新设计。
展望未来,我会说在Hermione中可以为每个镜头设置一个屏幕截图区域,乍一看,切换到新引擎将解决所有问题。 但是我们仍然必须“粉碎”大型测试套件。 事实是视觉测试本质上是不稳定的(这可能是由于各种原因,例如网络滞后,使用动画或“火星上的天气”),而且没有自动重试很难做到。 Gemini和Hermione都对整个测试套件执行重试,并且测试套件的“厚度”越大,重试期间成功完成的可能性就越小,因为 在下一次运行中,先前成功完成的测试可能会失败。 对于较厚的测试套件,我们必须实现Gemini引擎中内置的替代重试方案,并且确实不希望在切换到新引擎时再次这样做。
因此,为了加快测试套件的加载,我们将整体捆绑包分成几部分,将应用程序状态的每个快照分配为单独的“片段”,并为每个案例分别“按需”加载。 现在,故事创建代码如下所示:
要创建故事,将使用StoryProvider组件(下面将给出其代码)。 使用
动态导入功能加载快照。 不同的故事彼此之间仅在状态图片上有所不同。 对于深色主题,将使用与浅色主题相同的快照生成自己的故事。 在故事书的上下文中,它看起来像这样:
StoryProvider组件接受一个回调以加载在其中调用import()函数的快照。 import()函数是异步工作的,因此您无法在加载故事后立即截屏-我们冒着删除空白的风险。 为了赶上下载结束的时刻,提供程序呈现标记DOM元素,该元素在下载的整个过程中向测试引擎发出信号,应将其与屏幕截图一起延迟:
另外,要减小包的大小,请禁用向包添加源映射的操作。 但是为了不失去调试故事的能力(您永远不知道是什么),我们在以下条件下执行此操作:
.storybook / webpack.config.js npm run build-storybook脚本将不带源映射的静态故事
书编译到storybook-static文件夹中。 在执行测试时使用。
npm run故事书脚本用于开发和调试测试故事。
消除视觉信息重复
如上所述,Gemini允许您为整个测试套件设置屏幕截图区域选择器,这意味着要完全解决在屏幕截图中复制视觉信息的问题,我们必须为每个屏幕截图制作自己的测试套件。 即使考虑到加载故事的优化,它在速度方面也不是太乐观,因此我们考虑更改测试引擎。
其实,为什么是赫敏? 目前,Gemini存储库已被标记为已弃用,迟早我们不得不“移动”到某个地方。 Hermione配置文件的结构与Gemini配置文件的结构相同,我们能够重用此配置。 Gemini和Hermione插件也很常见。 另外,我们能够重用测试基础架构-虚拟机和已部署的硒网格。
与Gemini不同,Hermione并非仅作为布局回归测试的工具。 它的浏览器操作功能更加广泛,仅受
Webdriver IO功能的
限制 。 与
mocha结合使用时
,此引擎比布局测试更方便用于功能测试(模拟用户操作)。 对于布局的回归测试,Hermione仅提供assertView()方法,该方法将浏览器页面的屏幕截图与参考进行比较。 屏幕截图可以限制为使用CSS选择器指定的区域。
在我们的案例中,每个故事的测试如下所示:
如果将第二个参数设置为true,则waitForVisible()方法(尽管其名称)使您不仅可以期望外观,而且还可以期待元素的隐藏。 在这里,我们使用它来等待标记元素被隐藏,以指示尚未加载数据快照,并且故事还没有准备好截图。
如果您尝试在Hermione文档中找到waitForVisible()方法,将找不到任何内容。 事实是,waitForVisible()
方法是Webdriver IO API方法 。 分别使用url()方法。 在url()方法中,我们传递特定故事而不是整个故事书的框架地址。 首先,这是必要的,这样故事列表就不会显示在浏览器窗口中-我们不需要对其进行测试。 其次,如有必要,我们可以访问框架内的DOM元素(webdriverIO方法允许您在浏览器上下文中执行JavaScript代码)。
为了简化测试的编写,我们对mocha-tests进行了包装。 事实是,在详细描述用于回归测试的测试用例时没有特别的意义。 所有测试用例都相同-“应等于标准具”。 好吧,我也不想在每个测试中重复代码以等待数据加载。 因此,所有“猴子”测试的相同工作都委派给包装函数,并且测试本身以声明性方式编写(好了,差不多)。 这是此函数的文本:
create-test-suite.js const themes = [ 'default', 'dark' ]; const rootClassName = '.explorer'; const loadingStubClassName = '.loading-stub'; const timeout = 2000; function createTestSuite(testSuite) { const { name, storyName, browsers, testCases, selector } = testSuite;
描述测试套件的对象将传递到函数的输入。 每个测试套件都是根据以下情况构建的:我们对主要布局进行截图(例如,实体卡的区域或实体列表的区域),然后以编程方式按下可以导致其他元素出现的按钮(例如,弹出面板或上下文菜单),然后“对截图进行截图” »每个这样的元素分别。 因此,我们模拟了浏览器中的用户操作,但并不是为了测试业务场景,而是为了“捕获”尽可能多的可视组件。 此外,屏幕快照中的视觉信息重复非常少,因为 屏幕截图是使用选择器“按点”拍摄的。 测试套件示例:
确定承保范围
因此,我们确定了速度和冗余度,仍然需要弄清测试的有效性,即确定测试覆盖代码的程度(这里的代码是指CSS样式表)。
对于测试故事,我们根据经验选择了最复杂的卡片,列表和其他元素,以便用一个屏幕截图涵盖尽可能多的样式。 例如,为了测试实体卡,选择了具有大量不同类型的控件(文本,数字,传输,日期,网格等)的卡。 不同类型实体的卡具有其自身的详细信息,例如,可以从文档卡中显示带有文档版本列表的面板,并且在任务卡中显示与此任务的对应关系。 因此,对于每种类型的实体,创建了自己的故事以及针对该类型的一组测试等。 最后,我们认为测试似乎涵盖了所有内容,但我们要比“喜欢”多一点信心。
要评估Chrome DevTools中的coverage,有一种名为Coverage的工具非常适合这种情况:

Coverage允许您确定使用浏览器页面时使用的样式或js代码。 关于绿色条纹使用情况的报告指示已使用的代码,红色-未使用。 如果我们使用“ hello,world”级别的应用程序,一切都会很好,但是当我们有成千上万行代码时该怎么办? Coverage开发人员很好地理解了这一点,并提供了将报告导出到可以通过编程方式处理的文件的功能。
我必须马上说,到目前为止,我们还没有找到一种自动收集覆盖程度的方法。 从理论上讲,这可以使用pupeteer无头浏览器来完成,但是pupeteer在硒的控制下不起作用,这意味着我们将无法重用测试代码。 因此,现在,让我们跳过这个非常有趣的主题,并使用笔。
在手动模式下运行测试后,我们会得到一份覆盖率报告,它是一个json文件。 在每个CSS,JS,TS等的报告中。 该文件指示其文本(在一行中)以及该文本中使用的代码的间隔(以该行的字符索引的形式)。 以下是一份报告:
coverage.json [ { "url": "http://localhost:6006/theme-default.css", "ranges": [ { "start": 0, "end": 8127 } ], "text": "... --theme_primary-accent: #5b9bd5;\r\n --theme_primary-light: #ffffff;\r\n --theme_primary: #f4f4f4;\r\n ..." }, { "url": "http://localhost:6006/main.css", "ranges": [ { "start": 0, "end": 610 }, { "start": 728, "end": 754 } ] "text": "... \r\n line-height:1;\r\n}\r\n\r\nol, ul{\r\n list-style:none;\r\n}\r\n\r\nblockquote, q..." ]
乍一看,找到未使用的CSS选择器并不困难。 但是,如何处理此信息? 确实,在最终分析中,我们不需要找到特定的选择器,而是找到我们忘记涵盖测试的组件。 一个组件的样式可以由十几个选择器设置。 结果,根据报告的分析结果,我们得到了数百个未使用的选择器,如果处理每个选择器,您可能会浪费很多时间。
在这里,正则表达式可以帮助我们。 当然,它们仅在满足css类的命名约定时才起作用(在我们的代码中,css类是根据BEM方法论来命名的-block_name_name_name_modifier)。 使用正则表达式,我们可以计算块名称的唯一值,这些值不再难于与组件关联。 当然,我们也对元素和修饰符感兴趣,但首先不是,首先我们需要处理更大的“鱼”。 以下是用于处理覆盖率报告的脚本
coverage.js const modules = require('./coverage.json').filter(e => e.url.endsWith('.css')); function processRange(module, rangeStart, rangeEnd, isUsed) { const rules = module.text.slice(rangeStart, rangeEnd); if (rules) { const regex = /^\.([^\d{:,)_ ]+-?)+/gm; const classNames = rules.match(regex); classNames && classNames.forEach(name => selectors[name] = selectors[name] || isUsed); } } let previousEnd, selectors = {}; modules.forEach(module => { previousEnd = 0; for (const range of module.ranges) { processRange(module, previousEnd, range.start, false); processRange(module, range.start, range.end, true); previousEnd = range.end; } processRange(module, previousEnd, module.length, false); }); console.log('className;isUsed'); Object.keys(selectors).sort().forEach(s => { console.log(`${s};${selectors[s]}`); });
我们通过首先放置从Chrome DevTools导出的coverage.json文件并将废品写入.csv文件来执行脚本:
节点coverage.js> coverage.csv您可以使用excel打开此文件并分析数据,包括确定测试覆盖的代码百分比。

代替简历
使用故事书作为进行视觉测试的基础已经充分证明了自己的理由-我们以较少的故事数量和最少的创建新成本的方式充分覆盖了CSS代码。
向新引擎的过渡使我们能够消除屏幕快照中的可视信息重复,从而大大简化了对现有测试的支持。
CSS代码的覆盖程度是可衡量的,并且会不时受到监控。 当然,存在一个大问题-如何不忘记此控件的必要性,以及在收集有关覆盖范围信息的过程中如何不遗漏某些东西。 理想情况下,我想在每次测试运行时自动测量覆盖程度,以便在达到指定的阈值时,测试会出错。 我们将为此工作,如果有消息,我一定会告诉您。