6、状态验证:assertIsDisplayed、assertTextEquals、assertExists

状态验证,说白了就是检查 UI 组件「对不对、在不在、显不显」。

我刚开始写 Compose 测试时,总觉得只要点一下按钮、输入几个字就完事了。后来发现,没有状态验证的测试就像闭着眼睛开车——你根本不知道 UI 到底呈现了什么。今天我们就来聊聊三个最核心的验证方法。

6.1 验证组件是否存在:assertExists

这是最基础的验证。你只需要确认某个组件确实被渲染到了屏幕上。

@Test
fun testButtonExists() {
    composeTestRule.setContent {
        Button(onClick = { }) {
            Text("提交")
        }
    }

    composeTestRule
        .onNodeWithText("提交")
        .assertExists()
}

嗯,这里要注意:assertExists 只检查组件是否在组件树中,不关心它是否可见。我遇到过一种情况——组件确实存在,但被其他布局挡住了,测试依然通过。所以它适合做「快速冒烟检查」。

适用场景:
  • 检查关键按钮是否渲染
  • 检查列表项是否生成
  • 检查动态加载的组件是否出现

6.2 验证组件是否可见:assertIsDisplayed

这个就比 assertExists 严格多了。它不光检查组件存在,还检查组件是否真正可见——说白了就是用户能不能看到它。

@Test
fun testDialogDisplayed() {
    composeTestRule.setContent {
        var showDialog by remember { mutableStateOf(true) }
        if (showDialog) {
            AlertDialog(
                onDismissRequest = { showDialog = false },
                title = { Text("提示") },
                text = { Text("操作成功") },
                confirmButton = { TextButton(onClick = { showDialog = false }) { Text("确定") } }
            )
        }
    }

    composeTestRule
        .onNodeWithText("操作成功")
        .assertIsDisplayed()
}

我个人习惯用 assertIsDisplayed 代替 assertExists,除非有特殊原因。为什么?因为用户只关心「能不能看到」,不关心「存不存在」。你想想看,一个被 Modifier.alpha(0f) 隐藏的组件,assertExists 会通过,但用户根本看不见。

避坑指南:
  • assertIsDisplayedModifier.size(0.dp) 的组件会失败
  • Modifier.alpha(0f) 的组件也会失败
  • Modifier.offset(10000.dp) 移出屏幕的组件同样失败

6.3 验证文本内容:assertTextEquals

文本验证是 UI 测试里最频繁的操作。我见过有人用 assertExists 检查文本,但那样只能确认「有文字」,不能确认「是什么文字」。

@Test
fun testGreetingText() {
    composeTestRule.setContent {
        var name by remember { mutableStateOf("小明") }
        Text("你好,$name")
    }

    composeTestRule
        .onNodeWithText("你好,小明")
        .assertTextEquals("你好,小明")
}

这里有个细节:assertTextEquals 默认是精确匹配。如果你只想匹配部分内容,可以用 assertTextContains。我曾经在测试一个长文本时,因为末尾多了一个空格,测试挂了半小时才找到原因——嗯,从那以后我写文本验证都会先打印一下实际内容。

调试小技巧:

在测试里加一句 .printToLog("TEST"),可以打印出当前节点的所有文本信息。我每次写新测试都会先用它看看实际内容长什么样。

6.4 三种验证的对比

方法 检查内容 严格程度 典型场景
assertExists 组件是否在树中 快速冒烟、检查动态组件是否生成
assertIsDisplayed 组件是否可见 弹窗、加载状态、用户交互元素
assertTextEquals 文本内容是否匹配 表单验证、错误提示、数据展示

6.5 组合使用:一个完整的测试案例

实际项目中,很少只用一种验证。我一般会组合使用,像这样:

@Test
fun testLoginFormValidation() {
    composeTestRule.setContent {
        LoginScreen()
    }

    // 1. 先确认表单组件存在且可见
    composeTestRule
        .onNodeWithTag("username_input")
        .assertIsDisplayed()

    composeTestRule
        .onNodeWithTag("password_input")
        .assertIsDisplayed()

    // 2. 输入错误信息
    composeTestRule
        .onNodeWithTag("username_input")
        .performTextInput("")

    composeTestRule
        .onNodeWithTag("login_button")
        .performClick()

    // 3. 验证错误提示文本
    composeTestRule
        .onNodeWithText("用户名不能为空")
        .assertIsDisplayed()
        .assertTextEquals("用户名不能为空")
}

你看,这个测试里三种验证都用上了。先确认输入框可见,再点击按钮,最后验证错误提示的内容和可见性。每一步都有明确的目的。

6.6 知识体系总览

下面这张图总结了状态验证的核心逻辑:

状态验证核心逻辑 UI 状态验证 assertExists assertIsDisplayed assertTextEquals 组件是否在树中 快速冒烟检查 组件是否真正可见 用户交互验证 文本内容精确匹配 数据展示验证 组合使用原则 先确认存在 → 再确认可见 → 最后确认内容

6.7 常见问题与避坑

Q:为什么 assertExists 通过了,但 UI 上就是看不到?

大概率是组件被 Modifier.alpha(0f)Modifier.size(0.dp) 隐藏了。换成 assertIsDisplayed 就能发现。

Q:assertTextEquals 总是失败,明明文字看起来一样?

检查一下是否有不可见字符,比如空格、换行符。我建议先用 .printToLog() 打印实际内容,肉眼对比一下。

Q:多个相同文本的组件怎么验证?

onAllNodesWithText() 获取列表,然后按索引验证。或者给组件加 testTag,用 onNodeWithTag() 精确定位。

我的经验总结:

写状态验证时,记住一个原则:先确认存在,再确认可见,最后确认内容。这个顺序能帮你快速定位问题。如果 assertExists 都过不了,就别浪费时间检查可见性和文本了。


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