16、快照测试:Roborazzi入门、截图对比、基线管理

快照测试,说白了就是给UI拍张照片,然后拿它跟之前拍的照片对比。如果两张照片不一样,测试就挂了。这玩意儿在Compose里特别有用——你想想看,Compose的UI全是代码拼出来的,一个padding改错了,整个布局可能就歪了。传统的单元测试只能检查「按钮是否存在」,但快照测试能检查「按钮看起来对不对」。

我个人习惯把快照测试当作UI测试的最后一道防线。逻辑测试过了,交互测试过了,最后拍张照确认一下视觉上没问题。嗯,今天我们就来聊聊Roborazzi这个工具,它是目前Android圈子里做快照测试最趁手的家伙。

为什么需要快照测试?

我在项目中遇到过好几次这样的场景:改了一个颜色值,结果某个深藏不露的页面背景全变了。单元测试跑得绿油油的,但UI就是不对劲。为什么?因为单元测试只检查「值对不对」,不检查「看起来对不对」。

快照测试能帮你抓住这些漏网之鱼:

  • 视觉回归:代码改了,UI变了,测试立刻报警
  • 跨版本对比:同一个组件在不同系统版本上长啥样,一目了然
  • 团队协作:PR里带上截图对比,reviewer一眼就能看出问题
  • 文档即测试:生成的截图可以直接贴到设计文档里

核心思想:快照测试不是要替代单元测试,而是补上「视觉验证」这块拼图。两者配合使用,才能做到真正的质量保障。

Roborazzi入门

Roborazzi是专门为Android设计的快照测试库,支持Compose、View、甚至是整个Activity的截图。它的用法非常简洁,我直接上代码。

添加依赖

首先在项目的根build.gradle里加上插件:

// 根目录 build.gradle.kts
plugins {
    id("io.github.takahirom.roborazzi") version "1.8.0" apply false
}

然后在app模块里启用:

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

android {
    // 确保test构建类型启用了
    testBuildType = "debug"
}

dependencies {
    testImplementation("io.github.takahirom.roborazzi:roborazzi:1.8.0")
    testImplementation("io.github.takahirom.roborazzi:roborazzi-compose:1.8.0")
    testImplementation("io.github.takahirom.roborazzi:roborazzi-junit-rule:1.8.0")
}

小提示:Roborazzi依赖Robolectric,所以你的测试跑在JVM上,不需要模拟器。速度比真机测试快得多。

第一个快照测试

写一个最简单的Compose快照测试:

@RunWith(AndroidJUnit4::class)
class ProfileCardSnapshotTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun profileCard_snapshot() {
        composeTestRule.setContent {
            ProfileCard(
                name = "张三",
                avatar = "https://example.com/avatar.png",
                bio = "资深Android工程师"
            )
        }

        // 拍一张截图
        composeTestRule.onRoot().captureRoboImage("snapshots/profile_card.png")
    }
}

跑一次测试,Roborazzi会在build/reports/roborazzi目录下生成截图。第一次跑的时候,这张图就是「基线」(baseline)。以后每次跑测试,它都会拿新截图跟基线对比。

截图对比:到底比什么?

Roborazzi的对比不是简单的像素级对比——那样太死板了。它用的是结构相似性(SSIM)算法,能容忍一些微小的差异,比如抗锯齿导致的像素偏差。

对比结果有三种:

结果 含义 操作
✅ 通过 新截图与基线一致 无需处理
⚠️ 差异 存在视觉差异 检查差异报告,确认是否预期
❌ 失败 差异超过阈值 要么修复代码,要么更新基线

差异报告长什么样?Roborazzi会生成一张对比图,左边是基线,右边是新截图,中间用红色高亮标出差异区域。我曾经靠这个抓到过一个bug——某个按钮的圆角半径被改成了0,肉眼几乎看不出来,但对比图上一片红。

注意:默认的差异阈值是0.0,也就是零容忍。但在实际项目中,我建议设成0.01或0.02,给抗锯齿和字体渲染留点余量。不然每次换JDK版本,测试都可能飘红。

// 设置差异阈值
@get:Rule
val roborazziRule = RoborazziRule(
    options = RoborazziRule.Options(
        threshold = 0.02f  // 允许2%的差异
    )
)

基线管理:截图版本控制

基线文件就是你的「标准答案」。它们通常存在项目的src/test/assets/roborazzi目录下,或者你自定义的路径。这些文件必须提交到Git里,不然别人拉下来代码跑测试,没有基线可对比。

基线管理的核心原则就一条:基线只能由人工确认后更新。绝对不能自动覆盖。

什么时候更新基线?

  • UI设计变更:设计师改了样式,你改了代码,截图自然要更新
  • 修复了视觉bug:之前截图有瑕疵,现在修好了,基线要跟着变
  • 依赖库升级:Material3升级了,按钮样式变了,基线得重新拍

如何更新基线?

Roborazzi提供了两种方式:

  1. 命令行参数:跑测试时加上-P roborazzi.record=true,所有截图都会覆盖基线
  2. Gradle任务:执行./gradlew recordRoborazziDebug,专门用来录制基线
// 录制基线
./gradlew recordRoborazziDebug

// 验证基线(跑测试但不录制)
./gradlew verifyRoborazziDebug

我的习惯:在CI上只跑verifyRoborazziDebug,不允许自动录制。开发者本地改完UI后,手动跑一次录制,然后把新的基线文件提交到PR里。这样reviewer就能看到「哦,这个PR改了UI,基线也更新了」。

知识体系总览

下面这张图把快照测试的核心流程串起来了:

Roborazzi 快照测试流程 编写Compose测试 setContent + captureRoboImage 生成截图 build/reports/roborazzi/ 对比基线 SSIM算法 差异 < 阈值? ✅ 测试通过 UI无变化,继续集成 ⚠️ 差异报告 人工检查,决定是否更新基线 基线文件提交到Git,CI只验证不录制,开发者本地手动更新基线

实际项目中的避坑指南

快照测试看着简单,但用起来坑不少。我踩过的几个坑,分享给你:

坑一:动态内容导致截图不一致

如果你的UI里有当前时间、随机数、网络图片,每次截图都不一样。怎么办?

  • 时间:注入固定的时间戳,或者用Clock.fixed()
  • 网络图片:用Coil的FakeImageLoader,返回一张占位图
  • 随机数:用Random(seed)固定种子
// 固定时间
@Test
fun snapshot_withFixedTime() {
    val fixedClock = Clock.fixed(
        Instant.parse("2024-01-01T00:00:00Z"),
        ZoneOffset.UTC
    )
    Clock.withClock(fixedClock) {
        composeTestRule.setContent {
            TimestampDisplay()
        }
        composeTestRule.onRoot().captureRoboImage("snapshots/timestamp.png")
    }
}

坑二:字体渲染差异

不同操作系统、不同JDK版本,字体渲染结果可能不一样。我遇到过在macOS上截图通过,在Linux CI上截图失败的情况。

解决方案:在测试里指定字体,或者用Roborazzi的fontScale选项统一缩放比例。

composeTestRule.onRoot().captureRoboImage(
    "snapshots/profile_card.png",
    options = RoborazziOptions(
        fontScale = 1.0f  // 固定字体缩放
    )
)

坑三:基线文件膨胀

每个截图文件大概几十KB到几百KB,如果测试用例多了,Git仓库会变得很胖。我的做法是:

  • 只保留必要的截图,不要每个状态都拍
  • 用Git LFS管理基线文件
  • 定期清理不再使用的基线

曾经踩过的坑:有一次我忘了把基线文件加入.gitignore的某个目录,结果CI上每次跑测试都重新生成截图,导致基线被覆盖。排查了半天才发现是构建缓存的问题。后来我养成了习惯:在.gitignore里明确排除build/目录下的所有截图,只保留assets/roborazzi里的基线。

总结

Roborazzi让快照测试变得特别简单——写个Compose测试,拍张照,剩下的交给工具去对比。但工具再简单,也得靠人来把控基线更新的时机。记住一句话:基线是标准答案,不是自动生成的产物

我个人建议你在项目里先挑几个核心组件做快照测试,比如登录页、个人主页、列表卡片。跑通了流程,再逐步铺开。别一上来就给所有页面都加上,那样基线文件管理起来会让人崩溃。

好了,快照测试就聊到这儿。去试试Roborazzi吧,你会发现它抓视觉bug的能力,比肉眼靠谱多了。


公众号:蓝海资料掘金营,微信deep3321