18、截图测试与视觉回归:Screenshot测试工具

说实话,截图测试是我在团队里推广得最费劲、但回报也最明显的一项实践。我记得第一次跟产品经理说「我们要做截图测试」时,对方一脸懵——「截图?那不是手动点一下就行吗?」嗯,后来他亲眼看到一次UI重构后,十几个页面同时出现像素级偏差,才明白这玩意儿有多重要。

今天我们就来聊聊截图测试与视觉回归。说白了,就是让机器替你的眼睛干活,自动对比「这次改完的界面」和「上次正确的界面」有没有差异。

为什么需要截图测试?

你想想看,传统的单元测试能测逻辑、测状态、测数据流,但它测不了「这个按钮是不是往左偏了2像素」。UI测试虽然能点一点,但很难发现「背景色从#FFFFFF变成了#FEFEFE」这种细微变化。

截图测试正好补上这个缺口。它的核心逻辑很简单:

  • 先拍一张「黄金截图」——也就是你确认没问题的版本
  • 每次改完代码,再拍一张新的
  • 两张图逐像素对比,有差异就报错

我在项目中遇到过最典型的场景:设计师改了一个间距值,从16dp改成14dp,开发改完觉得「差不多」。结果截图测试一跑,十几个列表项全部标红。嗯,这就是视觉回归的价值。

主流工具:Shot vs Paparazzi

Android生态里,截图测试工具主要有两派。我两个都用过,各有各的脾气。

特性 Shot Paparazzi
运行方式 需要模拟器或真机 纯JVM运行,无需设备
截图速度 较慢(依赖设备渲染) 极快(纯CPU渲染)
对比精度 像素级 像素级
适用场景 完整UI、系统交互 组件级、快速反馈
CI友好度 需要GPU支持 无特殊要求

我个人习惯是:组件级别的截图用Paparazzi,页面级别的截图用Shot。为什么?Paparazzi跑得快,改一行代码几秒钟就能看到结果,适合开发阶段的快速迭代。Shot更接近真实设备,适合上线前的最终验证。

配置截图测试:以Shot为例

咱们直接上手。先加依赖:

// build.gradle (app module)
plugins {
    id 'com.karumi.shot' version '5.14.0'
}

android {
    // 确保testBuildType是debug
    testBuildType = "debug"
}

dependencies {
    androidTestImplementation 'com.karumi:shot:5.14.0'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

然后写一个截图测试用例。嗯,这里要注意:截图测试通常放在androidTest目录下,因为它需要真实的Activity环境。

// ProfileScreenScreenshotTest.kt
@RunWith(AndroidJUnit4::class)
class ProfileScreenScreenshotTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(ProfileActivity::class.java)

    @Test
    fun captureProfileScreen() {
        // 等待数据加载
        onView(withId(R.id.profile_container))
            .check(matches(isDisplayed()))

        // 截图并对比
        screenshot("profile_screen_default")
    }
}

第一次运行会生成黄金截图,存在app/src/test/screenshots目录下。之后每次运行都会自动对比。

小技巧:我建议把黄金截图也纳入版本控制。这样团队成员拉代码时,能直接看到「预期效果」。而且CI上跑失败时,可以快速定位是「代码改错了」还是「黄金截图该更新了」。

测试UI一致性:不只是像素对比

很多人以为截图测试就是「两张图叠一起看有没有不同」。其实没那么简单。我在项目中遇到过几个坑:

  • 字体渲染差异:不同Android版本对字体的渲染有细微差别,尤其是中文字体。解决方案是统一使用自定义字体,或者在测试设备上固定系统字体。
  • 动画干扰:如果截图时动画还没结束,结果就是两张完全不同的图。我习惯在截图前加一个Thread.sleep(500),或者用Animator.setDuration(0)禁用动画。
  • 系统UI遮挡:状态栏、导航栏的高度在不同设备上不一样。建议截图时用ActivityScenario设置全屏模式。
避坑指南:我曾经因为没处理动画,导致截图测试在CI上一直失败。排查了半天才发现是某个按钮的旋转动画还没停。从那以后,我所有截图测试的基类里都加了一行Animator.setDurationScale(0f)

处理不同屏幕尺寸

这是截图测试里最头疼的问题。一张截图在1080p上通过,换到720p上就全红了。怎么办?

我的做法是:按屏幕尺寸分组管理黄金截图

// 在测试中指定设备配置
@Test
fun captureProfileScreenOnPhone() {
    // 使用小屏设备配置
    activityRule.scenario.onActivity { activity ->
        activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
    }
    screenshot("profile_screen_phone")
}

@Test
fun captureProfileScreenOnTablet() {
    // 使用大屏设备配置
    activityRule.scenario.onActivity { activity ->
        activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
    }
    screenshot("profile_screen_tablet")
}

然后在CI上,我会配置多个模拟器,分别跑不同尺寸的截图测试。每个尺寸都有自己的黄金截图目录。

核心原则:截图测试不是「一张图打天下」,而是「一组图覆盖所有关键尺寸」。我一般会选3个尺寸:小屏手机(720p)、主流手机(1080p)、平板(1280p以上)。

CI集成截图测试

截图测试不上CI,基本等于白做。为什么?因为本地跑过了,别人一改又坏了。CI能保证每次提交都自动检查。

以GitHub Actions为例,配置很简单:

# .github/workflows/screenshot.yml
name: Screenshot Tests

on: [pull_request]

jobs:
  screenshot:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'

      - name: Run Shot screenshot tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 30
          target: google_apis
          arch: x86_64
          script: ./gradlew executeScreenshotTests -Pshot.testFormat=PNG

      - name: Upload screenshot diffs
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: screenshot-diffs
          path: app/build/reports/shot/**/*.png

这里有个关键点:失败时一定要上传差异图。这样PR的审查者能直接看到「哪里不一样」,不用自己再跑一遍。

CI上的小坑:模拟器在CI上跑截图测试,有时候会因为GPU渲染问题导致截图全黑。我建议在模拟器配置里加上-gpu swiftshader_indirect,或者直接用Paparazzi这种纯CPU方案。

知识体系总览

下面这张图是我自己整理的截图测试知识体系,你可以对照着看看自己掌握了哪些:

截图测试知识体系 截图测试工具 Paparazzi Shot 配置截图测试:依赖、规则、黄金截图管理 UI一致性测试 多屏幕尺寸处理 CI集成:自动化、差异报告、失败回传 从工具选型到CI落地,形成完整的视觉回归闭环

从这张图能看出来,截图测试不是孤立的技术点。它从工具选型开始,经过配置、核心能力落地,最后一定要跟CI打通,才能形成闭环。

写在最后

截图测试是我在团队里推得最成功的质量保障手段之一。它不像单元测试那么「抽象」,也不像UI自动化测试那么「脆弱」。一张图摆在那,过没过一目了然。

当然,它也有缺点——维护成本高、对设备敏感、偶尔会有误报。但如果你能做好尺寸分组、处理好动画和字体差异,它带来的收益绝对值得投入。

嗯,今天就聊到这。下次你在PR里看到截图测试全绿通过时,那种感觉,真的很爽。


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