4、快照生成与存储:文件格式(PNG)、存储路径、命名约定、版本控制
快照生出来只是个开始,怎么存、怎么命名、怎么管版本,才是真正考验工程水平的地方。我自己被这个问题坑过不止一次——早期一个项目里,开发者随便拉个文件名,结果CI上一跑,基线快照和失败快照混在一起,排查问题简直像大海捞针。后来我们重新设计了存储体系,才算彻底摆脱混乱。
为什么选 PNG?
你可能会问,为什么非得是PNG?用JPEG不行吗?嗯,这个问题我当初也纠结过。直接给结论:PNG是无损压缩,像素级对比时不会引入伪影。Compose绘制的UI就是精确的颜色和形状,任何有损格式都会破坏对比准确性。
| 格式 | 压缩方式 | 适用场景 | 我的建议 |
|---|---|---|---|
| PNG | 无损 | 快照回归(必须) | 唯一选择 |
| JPEG | 有损 | 照片类,不适用 | 不要用 |
| WebP | 可无损也可有损 | 可考虑(但需工具支持) | 慎用,生态不统一 |
| BMP | 无压缩 | 调试用,体积大 | 不推荐 |
另外要注意:PNG还可以保留透明度(Alpha通道)。如果你的Compose界面用到了透明背景或半透明元素,JPEG一压缩就全废了。所以我在项目中直接硬性规定——快照必须输出PNG,且带Alpha通道。
Bitmap.Config.ARGB_8888,保证颜色深度和透明度完整。
存储路径:别让快照掉进垃圾堆
我见过最糟糕的做法:把所有快照扔到 /build/screenshots/ 下,文件名靠时间戳区分。结果CI跑一次积压几百张,没人知道哪张是基线的、哪张是对比失败的。
一个清晰的路经方案应该包含:模块名 → 测试类 → 测试方法 → 设备/配置。举个例子:
app/build/reports/snapshots/
├── baseline/
│ └── com.example.ui.LoginScreen/
│ └── LoginScreen_initial.png
└── failures/
└── api33_sw600dp/
└── com.example.ui.LoginScreen/
└── LoginScreen_initial.png
这里「baseline」放基线,「failures」下按设备维度再细分。为什么要加上 api33_sw600dp 这样的配置标识?因为不同屏幕尺寸、不同API级别下,Compose的渲染可能不一样。我有个项目就是没加这个维度,结果同事在平板设备上跑出了绿屏,却跟手机基线一比就飘红,浪费时间排查。
命名约定:一眼看懂快照
文件名是快照的身份证。我的习惯是:{测试类简称}_{场景描述}_{配置后缀}.png。配置后缀用来区分多设备,比如 _light, _dark, _tablet。
| 文件名 | 含义 |
|---|---|
LoginScreen_initial_light.png |
LoginScreen测试类,初始状态,浅色主题 |
LoginScreen_error_dark.png |
错误提示状态,深色主题 |
ProfileCard_expanded_tablet.png |
展开状态的ProfileCard,平板宽度 |
你可能会问:直接在测试方法里用 @TestName("initial_light") 这种注解不行吗?行,但我建议文件名里不要带 @ 符号或空格,有些文件系统在CI上会出问题——我曾经因为文件名里有个冒号,在Linux CI上死活跑不通,排查了半天。
另外,我建议所有文件名统一小写字母 + 下划线,避免大小写敏感导致跨平台问题。Windows不区分大小写,但Linux区分,你想想看,如果基线快照叫 LoginScreen.png,新生成的叫 loginscreen.png,Git上就会显示两个文件,对比器根本找不到匹配项。
版本控制:快照也是代码
很多团队把快照扔进 .gitignore,觉得是二进制文件没必要管。但我告诉你:快照回归的核心就是基线快照要作为版本控制的一部分。否则你没法回溯「上次UI是长这样的」。
我的做法:
- 基线快照全部进Git仓库,跟测试代码放在一起。
- 每次修改Compose代码后,如果基线更新,必须提交新的基线图片。
- 对比失败产生的临时快照(failures目录)不进版本控制,它们只是CI过程的中间产物。
快照生命周期的整体视图
下面这张图总结了从生成到存储再到版本控制的完整流程,你可以把它挂在团队Wiki上。
代码实践:生成文件名
直接上代码,这是我项目中提取的片段:
fun generateSnapshotFileName(
testClassName: String,
scenario: String,
configuration: DeviceConfiguration
): String {
val configSuffix = buildString {
if (configuration.isDarkTheme) append("_dark") else append("_light")
if (configuration.screenWidthDp >= 600) append("_tablet")
append("_dpi${configuration.densityDpi}")
}
return "${testClassName}_${scenario}$configSuffix.png"
}
注意我把 densityDpi 也加进去了。为什么?因为同一个Composable在不同DPI下渲染结果可能不同——比如图标大小、间距。我曾经踩过这个坑:一位同事在模拟器上生成基线,另一位在真机上跑测试,DPI不同导致快照偏移1像素,对比失败。加上DPI标识后,这种问题就再也没出现。
总结一张检查表
- ✅ 输出格式:PNG(ARGB_8888)
- ✅ 存储路径:模块/测试类/测试方法/配置维度
- ✅ 文件名:
{类}_{场景}_{主题}_{设备}.png,全小写下划线 - ✅ 基线快照:提交到Git仓库,禁止手动修改
- ✅ 失败快照:临时目录,不加入版本控制
做到这几点,你的快照管理就能达到生产级别。下一级就是如何把这些快照跑进CI流水线了——不过那是后面章节的内容,今天先把存储这关守好。