移动应用的快照测试:原理、工具和示例

作者: IT Sectr 发布日期: 2026-04-07 阅读时间: 9 分钟

快照测试是一种自动化用户界面检查方法,它将屏幕的当前状态与上一次测试运行中保存的基准图像(snapshot)进行比较。任何视觉差异都会被记录为需要开发人员确认的更改。与检查元素是否存在的UI测试不同,快照测试捕获像素级别的变化——偏移、颜色偏差和布局问题。根据 Android Developers, 2024快照测试能检测到传统UI测试遗漏的30%的视觉回归,使其成为维护一致界面不可或缺的工具。

要点

  • 快照测试是将当前屏幕渲染与基准图像进行比较以发现视觉回归的方法。
  • Paparazzi — 适用于Android的库,无需启动模拟器即可在测试环境中渲染Compose和View组件。
  • Shot — 适用于Android的框架,在Instrumentation测试中截取真实屏幕的截图,支持多种分辨率。
  • SnapshotTesting — Point-Free提供的iOS库,不仅支持图像比较,还支持文本、JSON和Core Data数据。
  • 基准更新 — 在有意更改界面后执行的一次性命令,用新快照替换旧快照。

什么是快照测试?

快照测试(snapshot testing)是一种技术,测试渲染界面组件,将生成的图像保存为基准,并在后续运行时将当前渲染与该基准进行比较。如果图像匹配——测试通过。如果发现差异——测试失败,开发人员会收到带有更改像素高亮的差异图像。该技术源于Web开发(Jest snapshots)并已适配到移动平台。

快照测试的主要价值在于自动检测意外视觉变化。开发人员可能会更改全局主题中的配色方案并意外影响数十个屏幕。检查按钮和文本是否存在的UI测试不会注意到这一点。快照测试会捕获每个受影响屏幕上每个像素的变化,提供变更影响的完整图景。

根据Mobile DevOps Summit 2023的调查,在经典UI测试之外使用快照测试的团队将发布版本中的视觉缺陷数量减少了40%。这种方法在设计系统和基于组件的项目中特别有效,因为一个基础组件的更改可能影响应用程序的数十个屏幕。

快照测试与UI测试的区别

根本区别在于检查对象。UI测试检查界面元素的存在、状态和行为:“按钮可见”、“文本包含错误消息”、“点击后打开新屏幕”。快照测试检查整体外观:元素布局、间距、颜色、字体、阴影和圆角。快照测试回答的问题是“屏幕看起来是否符合预期?”,而UI测试回答的是“屏幕是否按预期工作?”

执行速度也不同。UI测试在模拟器或真实设备上运行,需要完全加载应用程序,每个场景耗时10秒到一分钟。快照测试基于Paparazzi等库在虚拟环境中渲染组件,无需启动模拟器,将测试时间缩短到100-500毫秒。完整的快照测试套件(50-100个屏幕)在2-5分钟内完成,而相同套件的UI测试则需要30-60分钟。

然而,快照测试不能替代UI测试。最佳策略是组合使用:快照测试覆盖视觉回归(每个屏幕在基本状态下的渲染),UI测试覆盖行为测试(点击场景、输入验证、导航)。这种组合在CI运行时间最小的情况下提供90%的界面正确性保证。

快照测试工具

在Android上,主要工具是Paparazzi和Shot。Cash App的Paparazzi在JVM上的测试环境中无需模拟器即可渲染组件,使用Layoutlib引力布局。Karumi的Shot在真实设备或模拟器上执行Instrumentation截图,并通过AShot库与基准进行比较,考虑分辨率和像素密度的差异。

适用于Android的Paparazzi

Paparazzi无需启动模拟器——通过Layoutlib在JVM上执行渲染,速度与单元测试相当。该库同时支持View系统和Jetpack Compose。对于Compose,使用修饰符paparazzi.snapshot { MyComposable() }。基准存储在src/test/snapshots中,每次运行时自动进行比较。最大差异百分比通过maxPercentDifference设置。

适用于iOS的SnapshotTesting

SnapshotTesting由Point-Free提供,不仅支持UIImage比较,还支持字符串、JSON、Data和整个Core Data存储的比较。这使其成为不仅适用于UI快照,还适用于检查JSON响应的序列化和解码的通用工具。对于SwiftUI,使用带有修饰符.image(on: .iPhone13)的assertSnapshot扩展。记录策略——record: true——在首次运行时创建基准。

跨平台方法

对于React Native,流行的解决方案是react-native-testing-library结合jest-image-snapshot。Web快照测试方法通过Node.js环境中的组件渲染并随后比较虚拟DOM的JSON快照,被移植到移动环境。这种方法比原生方法快,但精度较低——它不考虑字体和系统组件的平台特定渲染特性。对于Flutter,通过goldens工具包使用golden测试。

快照测试代码示例

让我们看看Android(Paparazzi)和iOS(SnapshotTesting)的快照测试。两个示例都检查一个组件的视觉效果——带有头像、姓名和状态的用户卡片。测试使用测试数据渲染组件并将结果与存储在存储库中的基准图像进行比较。

Android:使用Paparazzi进行快照测试

Paparazzi使用@Test注解和snapshot()方法捕获渲染效果。基准存储在src/test/snapshots文件夹中,下次运行时自动加载以进行比较。

kotlin
class UserCardSnapshotTest {

    @get:Rule
    val paparazzi = Paparazzi(
        theme = "Theme.MyApp",
        maxPercentDifference = 0.1
    )

    @Test
    fun userCard_defaultState() {
        val card = UserCard(
            name = "Alice Johnson",
            status = "Online",
            avatarUrl = "https://example.com/avatar.png"
        )
        paparazzi.snapshot(card)
    }

    @Test
    fun userCard_offlineState() {
        val card = UserCard(
            name = "Bob Smith",
            status = "Offline",
            avatarUrl = null
        )
        paparazzi.snapshot(card, name = "user_card_offline")
    }
}

iOS:使用SnapshotTesting进行快照测试

SnapshotTesting在assertSnapshot内部使用.snapshot()修饰符。该库自动确定格式——UIView使用UIImage,文本使用String,二进制数据使用Data。

swift
import SnapshotTesting
import XCTest

class UserCardSnapshotTests: XCTestCase {
    func testUserCardDefaultState() {
        let card = UserCardView(
            name: "Alice Johnson",
            status: "Online",
            avatarURL: URL(string: "https://example.com/avatar.png")
        )
        let controller = UIHostingController(rootView: card)
        assertSnapshot(matching: controller, as: .image(on: .iPhone13))
    }

    func testUserCardOfflineState() {
        let card = UserCardView(
            name: "Bob Smith",
            status: "Offline",
            avatarURL: nil
        )
        assertSnapshot(matching: card, as: .image(on: .iPhone13))
    }
}

快照测试工作流程

典型的工作流程包括四个阶段。首次运行(记录模式):所有快照测试在记录模式下运行——创建基准图像并保存到存储库。此阶段在初始测试设置或有意更改界面后执行。记录后,基准与代码一起提交——它们成为项目的一部分。

在后续运行中,测试在比较模式下工作:每次新的渲染都与基准进行比较。如果发现差异,会生成差异图像:绿色高亮显示与基准匹配的像素,红色显示不同的像素。开发人员研究差异并做出决定:如果更改是预期的(有意识的设计更改),则通过record命令更新基准;如果是意外的——则修复错误。基准更新通过一次命令执行:对于Paparazzi是`./gradlew recordPaparazzi`,对于SnapshotTesting是`assertSnapshot(record: true)`。

根据Spotify Engineering Blog (2022)的数据,使用上述工作流程的团队平均每个测试花费2分钟分析差异图像。对于50个快照测试的套件,完整的基准更新周期需要15-20分钟,这比在50个屏幕上手动验证视觉更改要快得多。

快照测试的限制和反模式

快照测试有根本性的限制。对环境敏感:同一组件在不同操作系统版本、屏幕密度和字体配置下可能呈现不同结果。在一台机器上创建的基准可能与CI服务器上的渲染不同。解决方案——使用固定的环境参数:Paparazzi使用特定版本的Layoutlib,SnapshotTesting使用精确的设备型号。

反模式#1:巨型快照——捕获整个屏幕的快照测试会在任何组件发生微小变化时失败。正确的方法是单独测试各个组件(按钮、卡片、输入字段)。每个组件独立测试,提供更改源的精确指示。反模式#2:忽略差异——不分析差异图像而自动更新基准会使快照测试的价值降为零。每个差异都需要开发人员的意识决策。

根据Better Engineering Blog (2023)的数据,快照测试在覆盖设计系统组件和关键屏幕的基本状态——空状态、填充状态、错误状态和边界状态——时带来最大的价值。由于渲染中时间戳的非确定性,通过快照测试覆盖动画和动态状态是低效的——对于此类场景,视频录制或手动QA检查更合适。

常见问题

快照测试能替代UI测试吗?

不能,快照测试检查外观,而UI测试检查界面行为。最佳策略是结合两种方法:快照用于视觉回归,UI测试用于检查场景和导航。快照回答的问题是“看起来是否正确”,UI测试回答的是“功能是否正常”。

基准图像需要多久更新一次?

基准在每次有意更改设计时更新:新的主题颜色、更改的间距、添加或删除元素。更新通过record模式执行,然后在代码审查中检查差异图像,以确保更改符合设计师的预期。

哪些组件应该用快照测试覆盖?

首先是设计系统组件——按钮、卡片、输入字段、模态窗口。然后是基本状态下的关键屏幕。不要用快照测试动画、WebView、地图和动态内容屏幕——对于它们,由于非确定性,快照会导致误报。

如何处理因不同操作系统版本导致的误报?

在record和test模式下使用相同的API Level。对于Paparazzi,在配置中指定特定的Layoutlib版本。对于SnapshotTesting,固定设备型号。在Android 14上创建的基准可能与Android 12上的渲染不同,因为系统字体和Material主题发生了变化。

CI中的快照测试——如何配置?

在CI中,快照测试在验证模式(verify)下运行。如果测试失败,CI在构建产物中显示差异图像。Record模式(更新基准)由开发人员在本地执行,或在带有手动触发的单独CI任务中执行。基准图像必须提交到存储库。

总结

  • 快照测试将组件的当前渲染与基准图像进行比较,发现像素级变化和视觉回归。
  • Paparazzi——适用于Android的快速工具,无需模拟器;SnapshotTesting——Point-Free提供的适用于iOS的通用库。
  • 快照测试不能替代UI测试——它们是互补的:快照用于视觉,UI测试用于行为。
  • Record模式创建基准图像;verify模式将其与当前渲染进行比较,并在存在差异时生成差异图。
  • 设计系统组件是快照测试的优先目标,因为它们的更改会影响许多屏幕。
  • 每个差异都需要开发人员的意识决策——不经分析自动替换基准会使测试的价值降为零。
  • 基准图像提交到存储库,与测试源码一样成为代码库的一部分。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读