在 Android 应用程序开发时,确保应用程序保持稳定对于提供良好的用户体验至关重要。为了实现这一目标,我们可以做的一件事是编写自动测试,根据应用程序的用户界面运行检查。在 Compose 中与之前编写测试的方式略有不同,Compose 提供了一组测试 API,用于查找元素、验证其属性以及执行用户操作。这些 API 还包括时间控制等高级功能。
语义
Compose 中的界面测试使用语义与界面层次结构进行交互。顾名思义,语义就是为一部分界面赋予意义。一个元素可以表示从单个可组合项到整个屏幕的任何内容。语义树与界面层次结构一起生成,并对其进行描述。
举个例子
有一个这样的按钮,它由一个图标和一个文本元素组成,默认语义树仅包含文本标签 “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
分别选择一个或多个节点,但也可以使用便捷查找器进行最常见的搜索,例如 onNodeWithText
、onNodeWithContentDescription
等。
查找单个节点
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"))
可以对最常见的断言使用便捷函数,例如 assertExists
、assertIsDisplayed
、assertTextEquals
等。可以在 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")
}
}
}
编写测试类 CounterTest
1. 获取 composeTestRule
,打印界面语义树。
@get:Rule
val composeTestRule = createAndroidComposeRule<MainActivity>()
2. 添加第一个测试函数,以验证是否显示计数器按钮。
@Test
fun counterInitTest() {
val buttonText = "Add 1"
composeTestRule
.onNodeWithText(buttonText)
.assertExists()
}
3. 运行第一个测试,结果通过!
4. 添加另一个测试来验证计数器是否正常工作,查找按钮并执行单击操作,然后断言使用以下代码将计数器值从 0 成功更改为 1。
@Test
fun counterIncrementTest() {
val buttonText = "Add 1"
val contentText = "This is 1"
composeTestRule
.onNodeWithText(buttonText)
.performClick()
composeTestRule
.onNodeWithText(contentText)
.assertExists()
}