测试 Compose 布局

1,350 阅读4分钟

在 Android 应用程序开发时,确保应用程序保持稳定对于提供良好的用户体验至关重要。为了实现这一目标,我们可以做的一件事是编写自动测试,根据应用程序的用户界面运行检查。在 Compose 中与之前编写测试的方式略有不同,Compose 提供了一组测试 API,用于查找元素、验证其属性以及执行用户操作。这些 API 还包括时间控制等高级功能。

语义

Compose 中的界面测试使用语义与界面层次结构进行交互。顾名思义,语义就是为一部分界面赋予意义。一个元素可以表示从单个可组合项到整个屏幕的任何内容。语义树与界面层次结构一起生成,并对其进行描述。

testing-semantic-tree.png

举个例子

testing-button.png

有一个这样的按钮,它由一个图标和一个文本元素组成,默认语义树仅包含文本标签 “Like”。这是因为,某些可组合项(例如 Text Button)已经向语义树公开了一些属性。还可以使用 Modifier 向语义树添加属性

MyButton(
    modifier = Modifier.semantics { contentDescription = "Add to favorites" }
)

设置

首先,需要添加所需的依赖项

// Test rules and transitive dependencies:
androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
// Needed for createComposeRule, but not createAndroidComposeRule:
debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")

其中包含 ComposeTestRule 和名为 AndroidComposeTestRule 的 Android 实现。通过此规则,可以设置 Compose 内容或访问 Activity。

@get:Rule
val composeTestRule = createComposeRule()
// createAndroidComposeRule<YourActivity>()

测试 API

与元素交互的方式主要有以下三种:

  • 查找器 可以选择一个或多个元素(或语义树中的节点),以进行断言或对这些元素执行操作。
  • 断言 用于验证元素是否存在或者具有某些属性。
  • 操作 会在元素上注入模拟的用户事件,例如点击或其他手势。

查找器

可以使用 onNode 和 onAllNodes 分别选择一个或多个节点,但也可以使用便捷查找器进行最常见的搜索,例如 onNodeWithTextonNodeWithContentDescription 等。

查找单个节点

composeTestRule
    .onNode(<<SemanticsMatcher>>, useUnmergedTree = false): SemanticsNodeInteraction

// Example
composeTestRule
    .onNode(onNodeWithText("Button"))
composeTestRule
    .onNode(hasText("Button"))
composeTestRule
    .onNodeWithContentDescription("Description")

查找多个节点

composeTestRule
    .onAllNodes(<<SemanticsMatcher>>): SemanticsNodeInteractionCollection
    
// Example
composeTestRule
    .onAllNodes(hasText("Button"))

使用未合并的树

某些节点会合并其子项的语义信息。

MyButton {
    Text("Hello")
    Text("World")
}

在测试中,我们可以使用 printToLog() 来显示语义树:

composeTestRule.onRoot().printToLog("TAG")

// 输出
Node #1 at (...)px
 |-Node #2 at (...)px
   Role = 'Button'
   Text = '[Hello, World]'
   Actions = [OnClick, GetTextLayoutResult]
   MergeDescendants = 'true'

需要匹配未合并的树的节点,可以将 useUnmergedTree 设为 true

composeTestRule.onRoot(useUnmergedTree = true).printToLog("TAG")

// 输出
Node #1 at (...)px
 |-Node #2 at (...)px
   OnClick = '...'
   MergeDescendants = 'true'
    |-Node #3 at (...)px
    | Text = '[Hello]'
    |-Node #5 at (83.0, 86.0, 191.0, 135.0)px
      Text = '[World]'

断言

可以通过对带有一个或多个匹配器的查找器返回的 SemanticsNodeInteraction 调用 assert() 来检查断言:

// Single matcher:
composeTestRule
    .onNode(matcher)
    .assert(hasText("Button")) // hasText is a SemanticsMatcher

// Multiple matchers can use and / or
composeTestRule
    .onNode(matcher).assert(hasText("Button") or hasText("Button2"))

可以对最常见的断言使用便捷函数,例如 assertExistsassertIsDisplayedassertTextEquals 等。可以在 Compose Testing 备忘单 中浏览完整列表。

操作

如需在节点上注入操作,请调用 perform…() 函数:

composeTestRule.onNode(...).performClick()

// performClick(),
// performSemanticsAction(key),
// performKeyPress(keyEvent),
// performGesture { swipeLeft() }

匹配器

可用于测试 Compose 代码的一些匹配器。

分层匹配器

分层匹配器可让您在语义树中向上或向下移动并执行简单的匹配。

fun hasParent(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnySibling(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyAncestor(matcher: SemanticsMatcher): SemanticsMatcher
fun hasAnyDescendant(matcher: SemanticsMatcher):  SemanticsMatcher

下面是一些使用这些匹配器的示例:

composeTestRule.onNode(hasParent(hasText("Button")))
    .assertIsDisplayed()

选择器

创建测试的另一种方法是使用选择器,这样可提高一些测试的可读性。

composeTestRule.onNode(hasTestTag("Players"))
    .onChildren()
    .filter(hasClickAction())
    .assertCountEquals(4)
    .onFirst()
    .assert(hasText("John"))

示例

介绍完这些 API 来看看实际运用吧!

编写一个简单的计数器

包含一个用于增加计数的 Button,和一个显示计数的 Text

@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    val text = "This is $count"
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        BasicText(text)
        Button(
            onClick = {
                count++
            },
        ) {
            BasicText(text = "Add 1")
        }
    }
}

Screen Shot 2022-01-03 at 10.13.28 PM.png

编写测试类 CounterTest

1. 获取 composeTestRule,打印界面语义树。
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()

Screen Shot 2022-01-03 at 10.27.36 PM.png

2. 添加第一个测试函数,以验证是否显示计数器按钮。
@Test
fun counterInitTest() {
    val buttonText = "Add 1"
    composeTestRule
        .onNodeWithText(buttonText)
        .assertExists()
}
3. 运行第一个测试,结果通过!

Screen Shot 2022-01-03 at 10.18.51 PM.png

4. 添加另一个测试来验证计数器是否正常工作,查找按钮并执行单击操作,然后断言使用以下代码将计数器值从 0 成功更改为 1。
@Test
fun counterIncrementTest() {
    val buttonText = "Add 1"
    val contentText = "This is 1"
    composeTestRule
        .onNodeWithText(buttonText)
        .performClick()
    composeTestRule
        .onNodeWithText(contentText)
        .assertExists()
}

Screen Shot 2022-01-03 at 10.30.17 PM.png

5. 运行所有的测试,结果都通过。🥳

Screen Shot 2022-01-03 at 10.21.05 PM.png