主题测试:MaterialTheme测试、颜色验证、字体验证

说实话,主题测试在Compose UI测试里经常被忽略。很多人觉得「主题嘛,跑起来看看颜色对不对就行了」。但我在项目中吃过亏——有一次主题换肤功能上线后,某个深色模式下的按钮文字颜色跟背景几乎融为一体,用户根本看不清。从那以后,我就把主题测试纳入了必测清单。

今天咱们就聊聊,怎么用代码来验证MaterialTheme的颜色、字体这些视觉属性。说白了,就是让测试替你的眼睛把关。

为什么需要主题测试?

你想想看,UI测试通常关注的是「点击按钮会不会跳转」「列表有没有显示数据」。但主题测试关注的是另一件事——「这个按钮在深色模式下是不是还能看清」。我个人的习惯是,只要项目里定义了多套主题,就一定要写主题测试。

主题测试能帮你发现:

  • 颜色值被意外修改(比如primary颜色从蓝色变成了灰色)
  • 字体大小或字重不符合设计规范
  • 深色/浅色模式切换后,对比度不足
  • 自定义主题属性没有被正确应用
核心思路: 主题测试的本质是「读取CompositionLocal中的主题值,然后断言它们符合预期」。

MaterialTheme的结构回顾

在Compose中,MaterialTheme通过三个核心对象来管理主题:

对象 作用 测试关注点
Colors 定义颜色调色板 primary, secondary, background, surface等
Typography 定义字体样式 fontFamily, fontSize, fontWeight等
Shapes 定义形状 small, medium, large的圆角值

测试时,我们需要在Compose测试环境中渲染一个组件,然后通过MaterialTheme.colorsMaterialTheme.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))
}
小技巧: 如果你不想每次都写Box+testTag,可以封装一个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就得重新生成基线图。我个人只在核心页面用快照测试,对于主题本身,还是用精确的断言更靠谱。

知识体系一览

下面这张图总结了主题测试的核心逻辑:

主题测试知识体系 MaterialTheme测试 颜色验证 字体验证 形状验证 primary background surface fontFamily fontSize fontWeight small medium large 核心:读取CompositionLocal → 断言具体属性值

实战建议

最后分享几个我在项目中积累的经验:

  • 颜色值用常量类管理:把Color(0xFF1976D2)这样的值定义成object常量,测试和业务代码共用,避免硬编码不一致。
  • 字体测试要覆盖中英文:中文字体跟英文字体可能不同。我建议至少写两个测试用例,分别验证中文和英文的fallback行为。
  • 不要过度测试:主题测试的目的是「防止意外修改」,而不是「验证设计稿」。每个颜色和字体测一次就够了,不用重复测。
  • 结合CI跑主题测试:把主题测试放在CI的快速验证阶段。毕竟颜色和字体变了,整个App的视觉都会受影响。
我的习惯: 每次修改主题文件后,先跑一遍主题测试,再跑UI截图对比。这样能快速定位问题是出在主题定义上,还是出在组件使用上。

主题测试看起来简单,但做细了能省很多视觉回归的麻烦。你想想看,与其让设计师一个一个截图对比,不如让测试代码在几秒内帮你搞定。这才是工程化的思路。

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