8、手势测试:点击、长按、滑动、拖拽测试
手势测试,说白了就是模拟用户的手指操作。你想想看,用户打开你的 App,第一件事就是点、滑、拖。这些操作要是出了问题,那体验可就全毁了。我在项目中见过太多因为手势没测好导致的线上事故——比如列表滑动卡顿、长按菜单弹不出来、拖拽排序乱掉。嗯,今天我们就来把这些坑一个个填上。
8.1 手势测试的核心思路
Compose 的 UI 测试框架提供了 SemanticsMatcher 和 SemanticsNodeInteraction 来定位元素,然后通过 performGesture 系列方法模拟手势。我个人习惯把手势测试分成两类:
- 简单手势:点击、长按、双击。这些操作通常触发一个明确的回调。
- 复杂手势:滑动、拖拽、缩放。这些操作涉及坐标变化和连续事件流。
不管哪一类,核心都是「模拟 → 断言」的循环。先模拟用户操作,再检查 UI 状态或业务逻辑是否正确。
关键点:手势测试不是测「手势本身」,而是测「手势触发的业务逻辑」。比如点击按钮后是否跳转、滑动后列表是否加载更多数据。
8.2 点击测试:最基础也最容易翻车
点击测试看起来简单,但我在项目中踩过不少坑。比如按钮被遮挡、点击区域太小、或者点击事件被父布局拦截了。来看一个标准示例:
@Test
fun testButtonClick() {
composeTestRule.setContent {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击次数: $count")
}
}
// 定位按钮并点击
composeTestRule
.onNodeWithText("点击次数: 0")
.performClick()
// 断言点击后文本更新
composeTestRule
.onNodeWithText("点击次数: 1")
.assertExists()
}
这里有个细节:performClick() 默认会等待动画和布局稳定。如果你的按钮有点击延迟或异步操作,记得加上 composeTestRule.waitForIdle()。
我曾经踩过的坑:用 performClick() 点击一个被 Modifier.alpha(0f) 隐藏的按钮,测试居然通过了!后来才发现,performClick() 只检查语义树中的节点是否存在,不检查可见性。所以一定要配合 .assertIsDisplayed() 使用。
8.3 长按测试:别忘了持续时间
长按测试和点击类似,但多了一个「持续时间」参数。默认的长按时间是 500 毫秒,但你可以自定义。来看一个长按弹出菜单的例子:
@Test
fun testLongPress() {
composeTestRule.setContent {
var showMenu by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
.pointerInput(Unit) {
detectTapGestures(
onLongClick = { showMenu = true }
)
}
) {
if (showMenu) {
Text("菜单已弹出")
}
}
}
// 执行长按
composeTestRule
.onNodeWithTag("long_press_box")
.performLongClick()
// 断言菜单出现
composeTestRule
.onNodeWithText("菜单已弹出")
.assertExists()
}
嗯,这里要注意:performLongClick() 默认使用 500ms 的长按时间。如果你的自定义手势要求更长的长按(比如 1000ms),可以用 performGesture { longClick(duration = 1000) }。
小技巧:长按测试最好配合 advanceTimeBy() 来模拟时间流逝,尤其是当你的长按逻辑依赖协程延迟时。
8.4 滑动测试:坐标与方向的艺术
滑动测试稍微复杂一点,因为你需要指定起始坐标和结束坐标。Compose 提供了 performTouchInput { swipe(...) } 方法。来看一个列表滑动的例子:
@Test
fun testSwipeToRefresh() {
composeTestRule.setContent {
val listState = rememberLazyListState()
LazyColumn(state = listState) {
items(100) { index ->
Text("Item $index", modifier = Modifier.testTag("item_$index"))
}
}
}
// 从屏幕中间向上滑动
composeTestRule
.onNodeWithTag("item_0")
.performTouchInput {
swipeUp()
}
// 断言第一个可见项不再是 item_0
composeTestRule
.onNodeWithTag("item_0")
.assertDoesNotExist()
}
这里有个坑:swipeUp() 默认从节点中心向上滑动。如果你的节点很小,滑动距离可能不够。我建议用 swipe(startX, startY, endX, endY) 指定绝对坐标,或者用 swipeWithVelocity 控制速度。
避坑指南:滑动测试时,记得考虑屏幕密度。不同设备上同样的 dp 值对应的像素不同。最好用 composeTestRule.density 做转换。
8.5 拖拽测试:从 A 点到 B 点
拖拽测试是手势测试里最复杂的。它涉及按下、移动、释放三个步骤。Compose 提供了 performTouchInput { drag(...) } 方法。来看一个拖拽排序的例子:
@Test
fun testDragAndDrop() {
composeTestRule.setContent {
var items by remember { mutableStateOf(listOf("A", "B", "C")) }
LazyColumn {
items(items, key = { it }) { item ->
Text(
text = item,
modifier = Modifier
.testTag("drag_$item")
.pointerInput(Unit) {
detectDragGestures { change, _ ->
change.consume()
}
}
)
}
}
}
// 拖拽 "A" 到 "C" 的位置
composeTestRule
.onNodeWithTag("drag_A")
.performTouchInput {
// 从 A 的中心开始,向下拖拽 200 像素
drag(
start = center,
end = Offset(center.x, center.y + 200f)
)
}
// 断言拖拽后的顺序
// 注意:这里需要根据你的业务逻辑做具体断言
}
我个人习惯在拖拽测试里加上 waitForIdle(),因为拖拽过程中可能会有动画或状态更新。另外,如果你的拖拽逻辑依赖 LazyColumn 的滚动,记得先滚动到目标位置再拖拽。
我曾经踩过的坑:拖拽测试时,如果目标节点被滚动出屏幕,拖拽会失败。一定要先确保两个节点都在可见区域内。可以用 scrollToNode() 或 scrollToIndex() 提前滚动。
8.6 手势测试的进阶技巧
除了基本的手势,还有一些进阶场景需要处理:
- 多点触控:用
performTouchInput { pinch(...) }模拟双指缩放。 - 手势冲突:当多个手势同时存在时,测试优先级和拦截逻辑。
- 边界条件:比如在屏幕边缘滑动、快速连续点击、超长拖拽等。
来看一个多点触控的示例:
@Test
fun testPinchToZoom() {
composeTestRule.setContent {
var scale by remember { mutableStateOf(1f) }
Box(
modifier = Modifier
.graphicsLayer(scaleX = scale, scaleY = scale)
.pointerInput(Unit) {
detectTransformGestures { _, _, zoom, _ ->
scale *= zoom
}
}
) {
Text("缩放测试")
}
}
// 模拟双指缩放
composeTestRule
.onNodeWithText("缩放测试")
.performTouchInput {
pinch(
start = Offset(100f, 100f),
end = Offset(200f, 200f),
start2 = Offset(300f, 300f),
end2 = Offset(200f, 200f)
)
}
// 断言缩放比例变化
// 注意:这里需要根据你的缩放逻辑做具体断言
}
8.7 知识体系总览
下面这张图总结了手势测试的核心知识点和它们之间的关系:
8.8 测试策略与最佳实践
最后,分享几个我在项目中总结的手势测试策略:
| 手势类型 | 测试重点 | 常见陷阱 |
|---|---|---|
| 点击 | 回调触发、状态更新 | 节点不可见但可点击 |
| 长按 | 持续时间、菜单弹出 | 时间精度不足 |
| 滑动 | 方向、距离、速度 | 屏幕密度差异 |
| 拖拽 | 起始/结束位置、顺序 | 节点滚动出屏幕 |
| 多点触控 | 缩放比例、旋转角度 | 手势冲突 |
我的建议:不要试图覆盖所有手势组合。优先测试用户最常用的操作路径,比如「点击按钮 → 滑动列表 → 长按删除」。把 80% 的精力放在 20% 的核心手势上,剩下的边界情况用手动测试补充。
好了,手势测试的内容就到这里。记住,测试不是目的,质量才是。每次写测试前,先问问自己:「这个手势操作如果出问题,用户会骂娘吗?」如果是,那就一定要测。
公众号:蓝海资料掘金营,微信deep3321