Monorepo(单一仓库)是一种源代码管理架构,其中多个项目、应用和共享库共存于单个 Git 仓库中,由 Nx、Turborepo 或 Lerna 等编排工具管理,这些工具根据依赖图优化构建和测试。
Monorepo 已经赢了。管理多个应用的前端团队——营销网站、管理面板、客户应用、合作伙伴门户——将它们放在同一个仓库中。共享代码更简单,设计系统更一致,横向更新更高效。
但对视觉测试来说,这是后勤噩梦。不是因为视觉测试在 monorepo 中不工作。它工作得很好。问题在于它工作得太好了:共享包的一个更改可能触发所有依赖项目的视觉测试。当你的 monorepo 包含 15、30 或 50 个项目时,"每次 commit 测试一切"不再是选项。
根本问题:依赖图
在 monorepo 中,项目并非独立的。它们共享包:设计系统、工具库、公共组件、共享配置。这些依赖形成了一个图——正是这个图使视觉测试变得复杂。
来看一个具体例子。你有一个 "ui-components" 包,包含按钮、表单和导航组件。这个包被 monorepo 中的 12 个应用使用。你修改了 "ui-components" 中一个按钮的 padding。视觉上,这个更改可能影响所有 12 个应用。每个包含按钮的页面都可能渲染不同。
如果你对 12 个应用都运行视觉测试,你可能要捕获数百张截图。CI/CD pipeline 需要 45 分钟。开发者等待。如果其中一个测试失败——一个因 headless 渲染问题 导致的误报——"ui-components" 的开发者不得不调查一个他根本不了解的应用中的失败。
这是不可持续的。而这正是没有视觉测试策略的 monorepo 中发生的事情。
测试变化的部分,而非全部:影响原则
Monorepo 中视觉测试的第一条规则理论上简单、实践中复杂:只测试被更改影响的部分。
现代 monorepo 工具——Nx 和 Turborepo 为首——能够计算依赖图并识别被更改"影响"的项目。当你修改 "ui-components" 包中的文件时,Nx 可以告诉你应用 A、C、F 和 K 依赖这个包,但 B、D 和其他的不依赖。
这是基础。但对视觉测试来说还不够。因为"应用 A 依赖 ui-components"并不意味着应用 A 的所有页面都使用了修改的组件。如果你改了按钮,只有包含按钮的页面受影响。不包含按钮的页面是稳定的。
Monorepo 中的智能视觉测试因此在两个级别上进行过滤。第一级:根据依赖图,哪些项目受更改影响。第二级:这些项目中哪些页面或组件实际使用了修改的元素。
第一级由 monorepo 工具解决。第二级困难得多——需要静态代码分析(哪些页面导入了哪个组件)或团队维护的显式映射(这个组件在这些页面上使用)。
共享包的现实
共享包是 monorepo 的优势也是劣势。它们实现一致性:单一设计系统,视觉组件的单一真实来源。但它们也创造了相当大的爆炸半径。
有三种类型的共享包为视觉测试带来特定问题。
UI 组件包。 最明显的情况。按钮、表单或模态框的更改在视觉上影响所有使用它们的页面。爆炸半径与组件的流行度成正比。到处使用的布局组件(Header、Footer、Sidebar)具有最大爆炸半径。
样式和 token 包。 Design tokens 的更改——颜色、间距、排版——在传统依赖图中通常不可见。如果你修改了 token 文件中的主色,依赖图看到的是 "design-tokens" 包的更改。但这个更改在视觉上影响一切:所有应用中的每个主按钮、每个链接、每个强调元素。爆炸半径是全面的。
具有视觉副作用的工具包。 不太明显:格式化日期、截断文本或计算布局的工具函数。文本截断函数的更改——从最多 100 个字符变为 80 个——对所有使用它的组件有直接的视觉影响。但依赖图不知道这是"视觉"更改。
有效的策略
在观察了数十个团队管理 monorepo 中的视觉测试后,以下是产生最佳结果的策略。
策略 1:分层视觉测试
将视觉测试组织为三个具有不同执行频率的层。
组件层。通过 Storybook 或等效工具隔离测试每个设计系统组件。这些测试仅在组件包更改时运行。它们很快(几分钟)并保护库本身。这是第一道安全网,在关于设计系统中的视觉测试的文章中有描述。
受影响页面层。测试受更改影响的应用页面,由依赖图识别。这些测试在每个 pull request 上运行,但仅针对受影响的项目。它们是你策略的核心。
完整层。测试所有应用的所有页面。这一层不在每个 PR 上运行——太昂贵了。每天运行一次(nightly),或在每次发布前运行。它捕获依赖图无法预测的回归。
策略 2:显式的组件到页面映射
维护一个将每个共享组件与使用它的页面关联的配置文件。当 Button 组件更改时,文件指示应用 A 的 /login、/signup、/checkout 和 /settings 页面使用该组件。只测试这些页面。
这种映射手动维护很繁琐。但可以通过静态代码分析自动生成——遍历每个页面的导入并追溯依赖。Nx 提供了简化这类分析的插件。
好处是可观的:当组件更改时,不是测试应用 A 的 200 个页面,而是只测试 12 个。Pipeline 从 30 分钟降到 3 分钟。
策略 3:带覆盖的共享 baseline
在具有共享设计系统的 monorepo 中,视觉 baseline(参考截图)有一个特点:共享组件在所有应用中具有相同的渲染(理论上)。因此你可以在包级别维护设计系统的 baseline,只在集成上下文改变时才在应用级别重新捕获 baseline。
具体来说:当你修改 Button 组件时,你在 ui-components 包中更新 Button 的 baseline。使用 Button 的应用继承这个新 baseline。只有 Button 在上下文中渲染不同的应用(由于特定 CSS、主题或覆盖)需要自己的 baseline。
应避免的经典错误
始终测试一切。 这是自然反应,也是最糟糕的。你的 pipeline 变慢,开发者因为噪音太多而忽略结果,真正的 bug 淹没在误报中。Monorepo 中的视觉测试必须精准如外科手术。
忽略依赖图。 一些团队独立于 monorepo 工具配置视觉测试。结果:测试不知道哪些项目受影响,要么测试一切要么什么都不测。将你的视觉测试工具与 Nx 或 Turborepo 集成。使用它们的 "affected" 命令触发正确的测试。
将所有 baseline 放在一个文件夹中。 视觉 baseline 应该与它们保护的项目在一起。如果你将所有 baseline 集中在根文件夹中,你就失去了粒度:不再知道哪个 baseline 属于哪个项目,清理过时 baseline 变得不可能。
在横向更新时忽视 baseline 版本控制。 当你修改影响所有项目的 design token 时,所有 baseline 必须同时更新。如果你在单独的 PR 中逐项目更新它们,你就创建了一个窗口期,其中一些项目有新 baseline 而另一些有旧的。测试变得不一致。
CI/CD 在 monorepo 视觉测试中的角色
CI/CD 配置至关重要。你的 pipeline 必须能做三件事。
第一,计算受更改影响的项目集。这是 Nx(nx affected)或 Turborepo(turbo run --filter)的工作。这一步决定了测试范围。
第二,按项目并行化视觉测试。如果三个应用受影响,并行运行这三个应用的测试,而非顺序运行。每个应用有自己的 baseline 集和结果报告。并行化是保持可接受 pipeline 时间的唯一方法。
第三,聚合结果并在必要时阻止 PR。即使测试是并行的,最终裁决必须是统一的:PR 通过或不通过。如果应用 A 有视觉回归而其他没有,PR 被阻止。开发者确切知道哪个应用受影响并可以调查。
当 no-code 简化一切
基于代码的视觉测试工具——Playwright、带视觉插件的 Cypress——很好地集成到 monorepo 中,因为它们存在于同一仓库中,紧挨着它们测试的代码。但它们增加了需要维护的测试代码、每项目配置和特定的 CI/CD 脚本。
像 Delta-QA 这样的 no-code 工具提供了不同的方法。它们不从源代码测试,而是从部署的 URL 测试。你配置每个应用的 URL,工具捕获截图并与 baseline 比对。Monorepo 不再是复杂性因素——工具只看到 URL,不是包。
缺点:你无法利用依赖图来过滤测试。优点:配置即时完成,没有测试代码需要维护,测试独立于仓库架构。对于没有专门测试开发者的团队,这通常是正确的权衡。
常见问题
Nx 还是 Turborepo 更适合 monorepo 中的视觉测试?
Nx 因其更详细的依赖图和静态分析插件而具有显著优势。nx affected 命令精确识别受影响的项目,Nx executors 允许将视觉测试任务集成到任务图中。Turborepo 更简单但提供的过滤粒度较低。
当 design token 更改影响一切时如何管理 baseline?
将其视为横向更新。创建一个专门的 PR,修改 token 并在一次操作中更新所有受影响的 baseline。运行完整层的视觉测试,批量批准预期的更改,然后合并。永远不要将 token 更新拆分到多个 PR 中——这为不一致打开大门。
monorepo pipeline 中的视觉测试应该花多长时间?
仅针对受影响的项目:标准 PR 触及单个项目时不到 10 分钟。共享包中的更改影响 3 到 5 个项目时不到 20 分钟。如果你的测试在 PR 上超过 30 分钟,你的过滤策略不够。完整层(nightly)可以更长——30 到 60 分钟是可接受的。
每个环境(dev、staging、prod)是否需要单独的 baseline?
是的,如果环境有不同的内容或配置。不是,如果渲染相同。实际上,为每个项目维护一个 baseline,在稳定的 staging 环境中捕获。与生产环境比对很诱人,但动态内容(日期、用户数据、A/B 测试)会产生太多误报。
monorepo 中的视觉测试是否与 Lerna 兼容?
Lerna 主要是版本管理和包发布工具。其依赖图不如 Nx 或 Turborepo 复杂。你可以使用它,但可能需要补充自定义脚本来识别受影响的项目。如果你正在启动新的 monorepo,优先选择 Nx 或 Turborepo。
如何防止开发者忽略视觉测试结果?
三条规则。将误报率保持在接近零——如果开发者经常看到误报,他们就不再关注。视觉测试失败时阻止 PR——不批准就不合并。保持 pipeline 时间短——如果视觉测试给 PR 增加了 45 分钟,开发者会找到绕过它的方法。
延伸阅读
Monorepo 是代码共享和一致性的优秀架构。但没有适应的视觉测试策略,它将每次更改变成一场抽奖:这次 commit 是否在一个我都不知道的项目中破坏了什么?这个问题的答案永远不应该是"我不知道"。它应该是自动的、快速的、可靠的。