主题测试:MaterialTheme测试、颜色验证、字体验证
说实话,主题测试在Compose UI测试里经常被忽略。很多人觉得「主题嘛,跑起来看看颜色对不对就行了」。但我在项目中吃过亏——有一次主题换肤功能上线后,某个深色模式下的按钮文字颜色跟背景几乎融为一体,用户根本看不清。从那以后,我就把主题测试纳入了必测清单。
今天咱们就聊聊,怎么用代码来验证MaterialTheme的颜色、字体这些视觉属性。说白了,就是让测试替你的眼睛把关。
为什么需要主题测试?
你想想看,UI测试通常关注的是「点击按钮会不会跳转」「列表有没有显示数据」。但主题测试关注的是另一件事——「这个按钮在深色模式下是不是还能看清」。我个人的习惯是,只要项目里定义了多套主题,就一定要写主题测试。
主题测试能帮你发现:
- 颜色值被意外修改(比如primary颜色从蓝色变成了灰色)
- 字体大小或字重不符合设计规范
- 深色/浅色模式切换后,对比度不足
- 自定义主题属性没有被正确应用
MaterialTheme的结构回顾
在Compose中,MaterialTheme通过三个核心对象来管理主题:
| 对象 | 作用 | 测试关注点 |
|---|---|---|
| Colors | 定义颜色调色板 | primary, secondary, background, surface等 |
| Typography | 定义字体样式 | fontFamily, fontSize, fontWeight等 |
| Shapes | 定义形状 | small, medium, large的圆角值 |
测试时,我们需要在Compose测试环境中渲染一个组件,然后通过MaterialTheme.colors或MaterialTheme.typography来获取当前值。
颜色验证:从「看起来对」到「测出来对」
颜色验证其实很简单。你只需要在Compose测试规则下,创建一个读取主题颜色的Composable,然后断言即可。
我常用的做法是写一个辅助函数:
@Composable
fun CaptureThemeColors(): Colors {
return MaterialTheme.colors
}
@Test
fun lightTheme_primaryColor_isCorrect() {
composeTestRule.setContent {
MyAppTheme(darkTheme = false) {
val colors = CaptureThemeColors()
// 这里只是占位,实际需要把colors存到某个可观察对象中
}
}
// 然后断言
}
但上面这种方式有点绕。更直接的做法是用ComposeTestRule配合onNodeWithTag来获取颜色值。我个人更推荐下面这种写法:
@Test
fun lightTheme_primaryColor_shouldBeBlue() {
composeTestRule.setContent {
MyAppTheme(darkTheme = false) {
Box(
modifier = Modifier
.testTag("colorBox")
.background(MaterialTheme.colors.primary)
)
}
}
// 验证背景色
composeTestRule
.onNodeWithTag("colorBox")
.assertBackgroundColor(Color(0xFF1976D2))
}
ColorCapture组件,把颜色值暴露给测试代码。
对于深色模式的验证,只需要切换darkTheme参数:
@Test
fun darkTheme_backgroundColor_shouldBeDark() {
composeTestRule.setContent {
MyAppTheme(darkTheme = true) {
Box(
modifier = Modifier
.testTag("bgBox")
.background(MaterialTheme.colors.background)
)
}
}
composeTestRule
.onNodeWithTag("bgBox")
.assertBackgroundColor(Color(0xFF121212))
}
嗯,这里要注意:不同版本的Material Design对深色背景的定义可能不同。我建议把颜色值定义成常量,方便统一维护。
字体验证:不只是「字体存在」
字体验证比颜色稍微复杂一点。因为字体涉及fontFamily、fontSize、fontWeight、lineHeight等多个维度。我曾经遇到过一个坑:设计师把标题字体改成了「Noto Sans SC」,但开发只改了fontFamily,忘了更新fontWeight,结果标题看起来比设计稿细了一圈。
所以字体验证要全面:
@Test
fun headline1_typography_shouldMatchDesign() {
composeTestRule.setContent {
MyAppTheme {
Text(
text = "测试标题",
style = MaterialTheme.typography.h1,
modifier = Modifier.testTag("headline1")
)
}
}
composeTestRule
.onNodeWithTag("headline1")
.assertFontSizeIs(34.sp) // 需要自定义断言
.assertFontWeightIs(FontWeight.Bold)
}
不过Compose测试默认没有assertFontSizeIs这样的API。你需要自己写扩展函数,或者用onNodeWithText配合captureToImage做像素级对比。我个人习惯用Semantics属性来暴露字体信息:
// 在Composable中暴露字体信息
Text(
text = "测试",
style = MaterialTheme.typography.body1,
modifier = Modifier.semantics {
testTag = "bodyText"
// 自定义语义属性
fontFamily = "Roboto"
fontSize = 16f
}
)
// 测试中读取
@Test
fun body1_fontFamily_isRoboto() {
composeTestRule
.onNodeWithTag("bodyText")
.assertHasFontFamily("Roboto")
}
SemanticsPropertyKey中注册。否则测试代码无法读取。这个步骤容易遗漏,我建议在项目初期就定义好一套通用的语义扩展。
整体主题快照测试
如果你觉得一个个颜色和字体去断言太繁琐,还有一个更粗暴的方法——快照测试。把整个主题渲染出来,截图保存,然后每次跑测试时对比截图。
不过快照测试的缺点是:稍微改一点UI就得重新生成基线图。我个人只在核心页面用快照测试,对于主题本身,还是用精确的断言更靠谱。
知识体系一览
下面这张图总结了主题测试的核心逻辑:
实战建议
最后分享几个我在项目中积累的经验:
- 颜色值用常量类管理:把
Color(0xFF1976D2)这样的值定义成object常量,测试和业务代码共用,避免硬编码不一致。 - 字体测试要覆盖中英文:中文字体跟英文字体可能不同。我建议至少写两个测试用例,分别验证中文和英文的fallback行为。
- 不要过度测试:主题测试的目的是「防止意外修改」,而不是「验证设计稿」。每个颜色和字体测一次就够了,不用重复测。
- 结合CI跑主题测试:把主题测试放在CI的快速验证阶段。毕竟颜色和字体变了,整个App的视觉都会受影响。
主题测试看起来简单,但做细了能省很多视觉回归的麻烦。你想想看,与其让设计师一个一个截图对比,不如让测试代码在几秒内帮你搞定。这才是工程化的思路。