3. 常用快照测试库对比:Shot、Paparazzi、Roborazzi

做 Compose 快照测试这些年,我踩过的坑不算少。最开始用的 Shot,后来换成 Paparazzi,现在主力是 Roborazzi。你可能会问:这三个到底有什么区别?我该选哪个?

别急,咱们一个个聊。我先把话说明白——没有银弹。每个库都有自己的脾气,选对了少加班,选错了……嗯,你懂的。

快照测试库对比图谱 选择标准 Shot Paparazzi Roborazzi JUnit规则 Compose原生 需真机/模拟器 布局渲染 无模拟器 速度最快 多平台支持 Gradle插件 CI友好 选择哪个?取决于你的项目规模、平台需求和CI环境

3.1 Shot——老大哥,成熟稳重

Shot 是 Jake Wharton 的杰作,主打基于 JUnit 规则截图对比。我在公司第一个快照测试项目就用它,当时觉得太方便了——加个 @Rule 就行。

@get:Rule
val shotRule = ShotRule()

@Test
fun testLoginButton() {
    composeTestRule.setContent {
        LoginButton()
    }
    shotRule.compareScreenshot(composeTestRule)
}

优点?

  • Compose 原生支持——直接和 ComposeTestRule 配合,不用额外配置。
  • 社区成熟——遇到问题 Stack Overflow 上基本能找到答案。
  • 代码侵入低——加个规则就能测,老项目改造压力小。

缺点也很明显:

  • 依赖模拟器或真机——CI 上跑的话,得先启动模拟器,慢啊。我记得有一次 Jenkins 上跑 Shot 测试,光等模拟器启动就用掉 3 分钟。
  • 多模块支持一般——每个模块都得单独配置规则,有点烦。
  • 更新较慢——最近几个 Compose 版本更新后,Shot 偶尔会有兼容性问题。

我曾经踩过的坑:用 Shot 时如果 Compose 版本升级到 1.3+,记得检查 setContent 是否正常工作。别问我怎么知道的——那次 CI 全红,我排查了一下午才发现是版本不兼容。

3.2 Paparazzi——快、准、轻

Paparazzi 不走寻常路。它不启动模拟器,而是直接利用 Android 的布局渲染管线来生成图片。说白了,它就是个纯 Java 库,能在本地 JVM 上跑。

看看它的用法:

@RunWith(ParameterizedTest::class)
class MyTest {
    @get:Rule
    val paparazzi = Paparazzi(
        deviceConfig = DeviceConfig.PIXEL_6,
        theme = "android:Theme.Material.Light"
    )

    @Test
    fun testButton() {
        paparazzi.snapshot {
            MyComposable()
        }
    }
}

优点:

  • 快!——没有模拟器启动开销,几秒就能跑完快照集。我负责的一个中型项目,300+ 测试用例,用 Paparazzi 跑全程只要 40 秒,之前用 Shot 得 5 分钟。
  • Gradle 配置简单——加个插件就完事,不需要复杂的依赖管理。
  • 支持多设备配置——可以一键切换不同分辨率和 DPI,测试覆盖率上去了。

缺点同样存在:

  • 渲染不完全等同于真机——字体、动画、硬件加速这些在 Paparazzi 里是模拟的,有时候会和真机有差异。
  • 对系统依赖强——版本升级时可能出现渲染引擎不兼容。
  • 只能测 View 级别的快照——不能像 Shot 那样直接测 Compose 的 setContent 外部逻辑,得包成 Composable 函数。

我个人习惯:Paparazzi 适合用在组件级(例如 Button、Card、TextField)的快照测试,因为这些组件渲染稳定,不容易和真机有偏差。如果想测整个页面,我会用 Roborazzi。

3.3 Roborazzi——后来者,集大成

Roborazzi 是我目前的主力工具。它结合了 Paparazzi 的纯 JVM 渲染能力,又吸收了 Shot 的 Compose 原生集成思路。更棒的是,它内置了 CI 友好的输出格式。

配置方式:

// build.gradle.kts
plugins {
    id("io.github.takahirom.roborazzi") version "1.8.0"
}

dependencies {
    testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.8.0")
}
// 测试代码
@Test
fun testCardSnapshot() {
    composeTestRule.setContent {
        MyCard()
    }
    composeTestRule.onRoot().captureRoboImage()
}

优点一览:

  • 支持多平台——Compose Multiplatform 也能用,跨 iOS 和 Android。
  • CI 好友——生成的报告可以直接和 GitHub Actions / GitLab CI 的 diff 展示集成。
  • 性能不错——介于 Paparazzi 和 Shot 之间,比 Shot 快不少。
  • 可定制性强——支持自定义 ImageComparator,比如你想忽略某些像素区域的差异。

缺点:

  • 起步晚——社区资源相对少,遇到边缘问题得靠自己看源码。
  • 配置稍多——尤其是多模块项目,每个模块的插件版本需要保持一致,否则会莫名其妙的编译错误。

注意:Roborazzi 依赖 Robolectric 2 以上版本。如果你项目里还在用老版 Robolectric,升级时会遇到一些冲突。我曾经花了半天时间排查 classloader 冲突——最后发现是 androidx.test 的版本没对齐。

3.4 一图胜千言——核心对比

维度 Shot Paparazzi Roborazzi
运行环境 模拟器/真机 JVM JVM(基于 Robolectric)
速度 慢(需模拟器) 极快 较快
CI 友好度 低(需模拟器加速) 高 高(内置 diff 报告)
Compose 支持 原生 需封装 原生
多平台 仅 Android 仅 Android Android + 桌面 + iOS(实验)
维护活跃度 中(更新慢) 中 高
学习成本 低 低 中等

3.5 我怎么选?——实际场景建议

讲了这么多,直接给你一个决策流程吧(这也是我每次新项目选型时用的路子):

  1. 先看速度要求——如果 CI 上你只有 30 秒的快照测试时间预算?别想了,直接上 Paparazzi 或 Roborazzi。Shot 肯定来不及。
  2. 再看项目复杂度——纯 Android 单 App,团队小,测几个核心页面?Shot 够用,简单粗暴。你是多模块项目,还要跑桌面端?Roborazzi 才是最稳的。
  3. 最后看团队习惯——大家都熟悉 JUnit Rules?Shot 最像老式测试写法,迁移成本低。想一步到位拥抱多平台?Roborazzi 值得投入。

我的实践经验:对于中型 Android 项目(20+ 模块),我最后选了 Roborazzi + Paparazzi 混合:核心 UI 组件用 Paparazzi(快,跑 CI 爽),复杂页面级快照用 Roborazzi(覆盖全)。两个工具都整合进同一个 Gradle 插件配置里,互不干扰。效果?CI 快照测试从 6 分钟降到了 1 分钟以内。

3.6 避坑合集

嗯,既然说到了实务,我再分享几个自己趟过的坑:

  • Shot 的图片对比默认是像素级的——一点点反走样差异就会导致失败。我曾用 compareScreenshot(composeTestRule, "golden", tolerance = 0.02) 加个容差才解决。
  • Paparazzi 在 CI 上渲染字体缺失——记得在测试模块添加 assets/fonts,或者用 paparazzi.fontScale 控制。
  • Roborazzi 的图片过大——默认是 PNG,动辄几 MB。开启 roborazzi.compressFormat = "webp" 可以大幅缩小。
  • 所有库都有的通病——快照测试生成的 golden 文件一旦需要更新,要小心版本冲突。我团队规定:golden 文件必须和代码一起提交到 Git,且每次更新必须附带测试截图对比。

最后说一句:快照测试不是银弹。它最适合回归验证 UI 是否意外改变。真正严格的功能逻辑,还是要靠单元测试和集成测试。嗯,把合适的工具用在合适的地方——这就是我的原则。