此文章尚未发布,搜索引擎不可见。
单页应用(SPA)视觉测试:更多状态、更多风险、更多测试理由

单页应用(SPA)视觉测试:更多状态、更多风险、更多测试理由

单页应用视觉测试:更多状态、更多风险、更多测试理由

核心要点

  • SPA 在机制上生成的视觉状态远多于传统多页面站点,而每个状态都可能包含回归
  • 无刷新导航、动画过渡和条件渲染创造了功能测试无法覆盖的视觉组合
  • 自动化视觉测试是捕获和验证 SPA 所有状态的唯一现实方法
  • 在 SPA 中忽视视觉测试,意味着接受你的大部分界面状态从未被验证

单页应用(Single Page Application),根据 MDN Web Docs(Mozilla Developer Network)的定义,是*「一种 Web 应用程序,通过动态重写当前页面而非从服务器加载全新页面来与用户交互,从而提供类似原生应用的更流畅用户体验」*(MDN, SPA — Single-page application)。

这个技术定义很优雅。但由此产生的 QA 现实远没有那么优雅。

因为通过动态重写页面而非加载新页面,SPA 在单个 HTML 文档中集中了远超传统站点所能产生的视觉状态数量。而每个状态都是可能出现显示错误的地方。

多页面站点曾有一个无人承认的优势

在 SPA 时代之前,每个 URL 对应一个独立的页面,完全从服务器加载。虽然慢,有时还会卡顿,但从 QA 角度看它有一个巨大优势:每个页面都是一个离散且稳定的状态。你可以截图、验证,然后转到下一个。

一个二十页的网站有二十个主要状态。你完全可以在白板上列出来,测试时逐一勾选。这是可管理的。

SPA 粉碎了这种简单性。一个中等复杂度的 React、Vue 或 Angular 应用不能简化为它的路由。每个路由可以根据加载的数据、用户交互、权限、错误条件和加载状态显示数十种不同的状态。

以 SPA 中的仪表板页面为例。它可以显示带 skeleton 的初始加载状态。然后是有数据的状态。然后是空数据状态(首次启动)。然后是网络错误状态。然后是带活跃筛选器的状态。然后是打开模态框的状态。然后是显示 tooltip 的状态。每个状态都有独特的视觉外观,你的用户都会看到。

将此乘以应用中的路由数量,你就会明白为什么手动测试 SPA 是一种幻想。

SPA 特有的视觉挑战

SPA 不仅仅是状态更多的网站。它们引入了传统多页面站点中不存在的整类视觉问题。

无刷新导航

在传统站点中,导航触发整个页面的重新加载。浏览器销毁现有 DOM,加载新 HTML,应用 CSS,执行 JavaScript。每个页面从干净状态开始。

在 SPA 中,导航修改现有 DOM 而不重新加载。组件被挂载和卸载。全局 state 在视图间持久化。副作用不断累积。

这创造了一个微妙但真实的问题:一个视图的外观可能取决于到达它的导航路径。一个组件在直接访问路由时正确挂载,但从另一个视图导航过来时可能显示不同,因为残留的全局状态影响了渲染。

通过 URL 直接访问每个路由的功能测试完全错过了这些与顺序导航相关的回归。只有重现真实用户路径并视觉捕获结果的测试才能检测到它们。

过渡和动画

SPA 大量使用过渡来平滑导航。一个组件不会瞬间消失:它以动画方式退出,同时下一个以动画方式进入。这些过渡是用户体验的关键组成部分。

它们也是视觉 Bug 的丰富来源。中断的过渡可能导致两个组件重叠。时序偏差可能创造内容闪烁。未正确完成的 CSS 动画可能让元素无限期地停留在中间视觉状态。

手动测试这些过渡需要持续的注意力和精确的时序。测试人员必须在正确的浏览器上、以正确的速度观察过渡的精确时刻。这对人类来说既脆弱又不可重现。

自动化视觉测试可以通过在动画周期的精确时刻截图来捕获中间过渡状态,检测人眼能看到但无法系统化记录的重叠、闪烁和不一致状态。

条件渲染

SPA 热衷于条件渲染。如果用户已登录就显示这个组件。如果功能处于 Beta 就显示这个横幅。如果购物车有商品就改变这个按钮。如果订阅已过期就隐藏这个区域。

每个条件在可能的视觉状态树中创建一个分支。五个独立条件理论上产生三十二种不同的视觉组合。十个条件则超过一千种。

没有人手动测试一千种组合。没有人在功能测试用例中穷举它们。然而它们确实存在,你的用户会遇到它们。

视觉测试不声称穷尽覆盖这些组合。但它覆盖的远比手动或功能测试多得多,因为每次视觉截图都验证整个屏幕,包括没有人想到要明确测试的元素。

异步加载状态

在多页面站点中,加载是一个二元事件:页面已加载或未加载。在 SPA 中,加载是一个持续的部分过程。一个组件可能已加载,而另一个仍在等待数据。页面同时处于"就绪"和"加载中"状态。

这些部分加载状态在视觉上是有意义的。你的 skeleton、spinner 和 placeholder 是完整的界面元素。一个尺寸不当的 skeleton 与最终内容大小不匹配会造成令人不快的视觉跳动。位置不当的 spinner 可能覆盖另一个组件。永不消失的 placeholder 是一个可见的 Bug。

视觉测试捕获这些中间状态并与参考进行比较,检测出功能测试(通常等待加载完成后才验证)永远看不到的不一致。

State management 是视觉状态的乘数

现代 SPA 架构依赖集中式 state management(Redux、Vuex、Pinia、NgRx、Zustand)。应用状态存储在全局 store 中,决定屏幕上显示什么。

这种架构模式对代码可维护性非常好。但对视觉可测试性来说很糟糕,因为它使可能的状态组合呈指数级增长。

你的 store 包含认证状态、购物车状态、用户偏好、缓存数据、待处理通知、feature flags。这些状态的每种组合都可能产生不同的视觉渲染。

一个已登录的用户,购物车为空,有未读通知,开启了暗黑模式——他看到的与一个使用亮色模式的匿名访客完全不同。两者都需要视觉测试。这些组合不是理论上的边缘情况。它们是你的用户每天经历的真实场景。

自动化视觉测试允许通过在每次截图前配置 store 状态来捕获这些组合。你不再只是测试页面:你在测试完整的应用状态,每个都有经过验证的视觉渲染。

Client-side routing 及其视觉陷阱

SPA 的 router 在客户端处理导航。它实现了无需重新加载的即时视图切换。但它也创造了特定的视觉陷阱。

第一个陷阱是滚动位置。在多页面站点中,每次导航都将滚动重置到顶部。在 SPA 中,滚动位置在视图间保持,除非 router 配置了重置。结果:从长页面导航到短页面的用户可能会看到空白屏幕,滚动到了内容之外。这是一个功能测试不会检测到但视觉测试能立即捕获的视觉 Bug。

第二个陷阱是 deep linking。用户可以通过分享链接或书签直接到达 SPA 的任何路由。即使没有正常的导航上下文,渲染也必须正确。但你的 SPA 可能需要在之前的路由上加载的数据。没有这些数据,组件可能显示为空或有意外的默认值。

第三个陷阱是后退导航。SPA 中浏览器的"后退"按钮不会重新加载上一页:它恢复历史状态。如果状态恢复不完整,显示的视图可能与原来在视觉上不同。将恢复的视图与原始视图进行比较的视觉测试能检测到这些不一致。

SPA 视觉测试的结构化方法

视觉测试 SPA 需要一种结构化方法,要考虑其特定的复杂性。

第一个维度是路由覆盖。应用的每个路由至少应该有一个默认状态的视觉参考截图。这是基础级别,相当于测试多页面站点的每个页面。

第二个维度是状态覆盖。对于每个路由,识别视觉上不同的状态:空状态、已加载状态、错误状态、加载状态。每个状态都应有自己的参考截图。

第三个维度是交互覆盖。模态框、下拉菜单、tooltip、通过交互打开的侧面板本身就是视觉状态。它们必须被触发和截图。

第四个维度是路径覆盖。某些视觉 Bug 只在特定导航上下文中出现。重现真实用户路径(登录、导航到仪表板、打开项目、返回列表)的测试能捕获这些上下文回归。

这种四维方法看起来令人望而生畏。如果你手动测试,确实如此。使用自动化视觉测试工具,每个维度都转化为测试配置,而非数小时的人工劳动。

SPA 需要更多视觉测试,而非更少

面对 SPA 的复杂性,一些团队产生了可以理解的诱惑:减少视觉测试投入。"状态太多了,不可能全覆盖,还是专注功能测试吧。"

这种反应恰恰与应该做的相反。你的应用视觉状态越多,视觉测试就越必要。正是因为 SPA 在视觉上很复杂,手动测试才不够用,视觉自动化才变得不可或缺。

功能测试验证你的应用做了它该做的。视觉测试验证它看起来像它该看起来的样子。在 SPA 中,外观根据状态不断变化,这种视觉验证不是奢侈品,而是必需品。

每一个未经视觉测试的状态都是可能悄悄回归的状态。在 SPA 中,未测试状态的数量很快就超过了已测试状态的数量——除非你使用能自动捕获它们的工具。


常见问题

视觉测试适用于所有 SPA 框架(React、Vue、Angular、Svelte)吗?

视觉测试与框架无关。它捕获浏览器渲染的最终外观,无论使用什么框架来生成它。无论你的 SPA 使用 React、Vue、Angular、Svelte 还是其他任何框架构建,结果都是一样的:在浏览器中渲染的 HTML、CSS 和 JavaScript。视觉测试在这个层面运作——最终渲染层面,这使它具有普遍兼容性。

如何在 SPA 视觉测试中处理动画和过渡?

动画是一个特殊挑战。最佳方法是在视觉测试截图期间禁用 CSS 和 JavaScript 动画,以获得稳定且可重现的比较。一些视觉测试工具原生提供此选项。或者,你可以捕获过渡前后的状态而非过渡中的,这消除了与动画时序相关的变异性,同时验证了开始和结束的视觉状态。

如何视觉测试依赖动态数据(API、数据库)的状态?

标准解决方案是 API mocking。你配置测试来拦截网络调用并返回确定性数据。这使你能可靠地重现每种视觉状态:数据已加载、数据为空、网络错误、加载缓慢。没有 mocking,你的视觉截图会随每次执行基于真实数据而变化,使比较成为不可能。

中型 SPA 应该测试多少视觉状态?

没有神奇数字,但一个实用的经验法则是每个路由至少覆盖三种状态:正常状态(数据已加载)、空状态(无数据)和错误状态。对于二十个路由的 SPA,这意味着至少六十个参考截图。加上交互状态(模态框、下拉菜单、面板),你很容易达到一百到一百五十个截图。看起来很多,但使用自动化工具完全可以管理。

视觉测试是否替代 SPA 中的单元测试和端到端测试?

不,这也不是它的目的。单元测试验证组件的业务逻辑。端到端测试验证完整的功能流程。视觉测试验证外观。这三个测试层级是互补的。视觉测试填补了其他两者留下的空白:检测在功能断言中不被注意的视觉回归。一个按钮可能功能正常但不可见。一个表单可能提交正确但布局损坏。只有视觉测试能捕获这些问题。

SPA 的视觉测试执行是不是太慢了?

现代 SPA 在几百毫秒内渲染。视觉截图本身只需要额外几十毫秒。图像比较几乎是即时的。对于一百个截图的测试套件,总时间通常为两到五分钟,包括状态间的导航时间。与完整 CI/CD 流水线的时长相比微不足道,比只能覆盖这些状态一小部分的手动测试快无数倍。


你的 SPA 有数十个无人测试的视觉状态?是时候改变了。

免费试用 Delta-QA →