HarmonyOS 6实战::多组件嵌套场景下,自动化测试覆盖复杂交互实践
先回顾一下我们之前的开发背景。
我们通过近10期的内容做了个AI旅行助手,用户可以在地图上规划路线、拖拽途经点、滑动查看详情面板。功能多,手势复杂------滑动地图、缩放、拖拽点、滑面板,好几个手势挤在一起。
之前我们专门写过一篇解决手势冲突的文章,当时调了好久才让它们和谐共处。
但问题没完。
每次改完代码,我都得手动点一遍所有功能------拖拽点会不会卡住?滑动面板还能不能吸到中档?地图缩放还跟手吗?
刚开始还能忍,后来功能越来越多,手动测试一遍要花十几分钟。更烦的是,有时候改了个看似无关的地方,某个角落的功能悄悄坏了,打补丁打累了。
于是决定引入UI自动化测试。后续计划也验证一下AI做自动化测试的可行性。
一、先看看我们要测什么

我们的AI助手页面UI有几项关键部分需要重点测试:
- 底部有个可拖拽的面板,三档吸附(低、中、高)
- 地图区域支持单指滑动、双指缩放
- 路线上的点可以长按拖拽调整位置
- 面板里有搜索框、功能按钮、足迹列表
需要测试的场景:
| 功能 | 测试点 | 之前的坑 |
|---|---|---|
| 拖拽面板 | 能否拖到高档、中档、低档 | 吸附不准,卡在半中间 |
| 地图滑动 | 单指滑动是否流畅 | 和拖拽点冲突 |
| 地图缩放 | 双指捏合是否响应 | 手势识别不稳定 |
| 拖拽路线点 | 长按后能否拖动 | 被地图滑动抢走 |
| 搜索输入 | 能否输入文字并搜索 | 键盘弹起后布局错乱 |
| 足迹列表 | 横向滚动是否正常 | 滚动和面板拖拽冲突 |
这些场景手动测一遍至少10分钟,而且容易漏。用自动化测试,跑一遍只要几秒钟。
二、UiTest引入与测试用例编写
鸿蒙提供了UiTest框架,放在@kit.TestKit里。核心就三个东西:
Driver :测试的"总指挥"。负责找控件、点按钮、滑动屏幕、按返回键。所有操作都是异步的,记得加await。
ON :用来描述"我要找什么控件"。可以按文本找、按ID找、按类型找。支持链式调用,比如ON.text('确定').type('Button')。
Component:找到控件后的操作对象。可以点它、双击它、长按它、往里面输文字。
一个典型的测试用例大概长这样:
typescript
let driver = Driver.create();
let button = await driver.findComponent(ON.id('confirm_btn'));
await button.click();
await driver.assertComponentExist(ON.text('操作成功'));
结构很清楚:找控件 → 操作 → 断言结果。
测试代码放在entry/src/ohosTest/ets/test/目录下。我们按功能模块分了几个文件:
entry/src/ohosTest/ets/test/
├── MapGesture.test.ets # 地图手势测试
├── DragPanel.test.ets # 拖拽面板测试
├── DragPoint.test.ets # 拖拽路线点测试
├── SearchInput.test.ets # 搜索输入测试
└── FootprintList.test.ets # 足迹列表测试
每个文件对应一个功能模块,互不干扰。
测试用例写在describe块里,每个it是一个独立的测试用例。
typescript
export default function mapGestureTest() {
describe('地图手势测试', () => {
beforeAll(async () => {
// 启动应用,进入地图页面
})
it('单指滑动地图', async (done) => {
// 测试代码
done();
})
it('双指缩放地图', async (done) => {
// 测试代码
done();
})
})
}
三、几个核心场景的测试代码
为了节约上手成本,我采用了华为官方的AI助手辅助生成代码+手动调试的策略完成了下面的测试用例。
测试拖拽面板吸附

面板有三个档位:低档150vp、中档269vp、高档(接近全屏)。我们的测试要验证:拖拽到不同位置,松手后能否吸到正确的档位。
typescript
// DragPanel.test.ets
it('拖拽面板到中档位置,松手后应吸附到中档', async (done) => {
let driver = Driver.create();
await driver.delayMs(2000); // 等待页面加载完成
// 找到面板的拖拽指示条
let dragBar = await driver.findComponent(ON.id('panel_drag_bar'));
let bounds = await dragBar.getBounds();
// 计算拖拽目标位置(向上拖拽到中档高度)
let startX = bounds.centerX;
let startY = bounds.centerY;
let targetY = startY - 200; // 向上拖拽200像素
// 执行拖拽
await driver.swipe(startX, startY, startX, targetY);
await driver.delayMs(500); // 等待吸附动画完成
// 验证面板高度是否等于中档高度269
let panel = await driver.findComponent(ON.id('bottom_panel'));
let panelBounds = await panel.getBounds();
let panelHeight = panelBounds.bottom - panelBounds.top;
expect(Math.abs(panelHeight - 269) < 10).assertTrue(); // 允许±10像素误差
done();
})
关键点:
- 用
getBounds()获取组件位置,计算拖拽起点和终点 - 拖拽后等待动画完成再断言
- 断言允许少量误差,避免像素精度问题
测试拖拽路线点

路线点长按拖拽是最容易出问题的地方------之前一直被地图滑动手势抢走。
typescript
// DragPoint.test.ets
it('长按路线点后拖拽,点应跟随手指移动', async (done) => {
let driver = Driver.create();
await driver.delayMs(2000);
// 找到路线点(假设路线点有特定ID)
let waypoint = await driver.findComponent(ON.id('waypoint_1'));
let bounds = await waypoint.getBounds();
// 长按路线点
await waypoint.longClick();
await driver.delayMs(300); // 等待长按触发
// 拖拽到新位置
let startX = bounds.centerX;
let startY = bounds.centerY;
let targetX = startX + 100;
let targetY = startY - 50;
await driver.swipe(startX, startY, targetX, targetY);
await driver.delayMs(500);
// 验证路线点位置已变化(通过检查坐标或地图上的标记)
let newWaypoint = await driver.findComponent(ON.id('waypoint_1'));
let newBounds = await newWaypoint.getBounds();
// 新位置的x坐标应该增加了
expect(newBounds.centerX > bounds.centerX).assertTrue();
done();
})
关键点:
- 用
longClick()模拟长按 - 拖拽后用
getBounds()验证位置变化 - 注意长按和拖拽之间的延时,太短可能识别成普通点击
测试地图手势
地图手势测试比较特殊------地图本身没有明确的控件ID,我们需要用坐标来模拟操作。
typescript
// MapGesture.test.ets
it('双指捏合应缩放地图', async (done) => {
let driver = Driver.create();
await driver.delayMs(2000);
// 获取屏幕尺寸
let display = await driver.getDisplayInfo();
let screenWidth = display.width;
let screenHeight = display.height;
// 双指捏合:两个手指从中心向中间收拢
// 手指1:从(centerX-100, centerY) 移动到 (centerX-50, centerY)
// 手指2:从(centerX+100, centerY) 移动到 (centerX+50, centerY)
// 注意:鸿蒙的swipe是单指,双指捏合需要用pinch方法
// 这里用坐标点模拟
let centerX = screenWidth / 2;
let centerY = screenHeight / 2;
// 先记录缩放前的可见区域(通过地图边界判断)
// 这里简化处理,实际可能需要读取地图组件的zoomLevel
// 执行捏合操作(具体API参考文档)
await driver.pinch(centerX, centerY, 200, 'in'); // 向内捏合
await driver.delayMs(1000);
// 验证缩放效果(通过检查地图上某个固定点的位置变化)
// 这里假设地图中心点变了,或者zoomLevel变了
done();
})
关键点:
- 地图没有ID,需要用坐标定位
- 双指操作需要用专门的
pinch方法 - 验证缩放需要读取地图状态或对比截图
测试搜索输入
搜索框的测试主要验证输入和清空功能。
typescript
// SearchInput.test.ets
it('在搜索框输入文字,应能正确显示', async (done) => {
let driver = Driver.create();
await driver.delayMs(2000);
// 找到搜索框
let searchInput = await driver.findComponent(ON.id('search_input'));
// 输入文字
await searchInput.inputText('故宫');
await driver.delayMs(500);
// 验证输入结果
let inputText = await searchInput.getText();
expect(inputText).assertEqual('故宫');
// 测试清空
await searchInput.clearText();
await driver.delayMs(300);
let clearedText = await searchInput.getText();
expect(clearedText).assertEqual('');
done();
})
关键点:
inputText()支持中英文getText()获取输入框当前内容clearText()一键清空
测试足迹列表横向滚动

足迹列表是一个横向滚动的Scroll,需要验证左右滑动能切换显示的内容。
typescript
// FootprintList.test.ets
it('横向滑动足迹列表,应切换显示不同城市卡片', async (done) => {
let driver = Driver.create();
await driver.delayMs(2000);
// 找到足迹列表容器
let listContainer = await driver.findComponent(ON.id('footprint_list'));
let bounds = await listContainer.getBounds();
// 记录滑动前的第一个城市
let firstCard = await driver.findComponent(ON.id('city_card_0'));
let firstName = await firstCard.getText();
// 横向向左滑动
let startX = bounds.right - 50;
let endX = bounds.left + 50;
let y = bounds.centerY;
await driver.swipe(startX, y, endX, y);
await driver.delayMs(500);
// 滑动后,原来的第一个卡片应该不可见
let oldCardExists = await driver.findComponent(ON.id('city_card_0')).catch(() => null);
expect(oldCardExists).assertNull();
// 新的卡片应该出现
let newCard = await driver.findComponent(ON.id('city_card_1'));
expect(newCard).assertNotNull();
done();
})
踩过的坑
坑1:控件生命周期相关问题
页面打开后,控件可能还没渲染完。直接findComponent会返回null。
解决方案:加delayMs或waitForIdle。
typescript
await driver.waitForIdle(4000, 5000); // 等待UI稳定
拖拽面板有吸附动画,动画没结束时断言会拿到错误的高度。
解决方案:拖拽后加延时等待动画完成。
typescript
await driver.swipe(...);
await driver.delayMs(500); // 等待动画
坑2:控件ID写错或者忘记加
测试代码里用ON.id('xxx')找控件,但实际页面上可能忘了给控件加.id('xxx')。
解决方案:写代码的时候就顺手给关键控件加上ID。
typescript
// 页面代码
Button('提交')
.id('submit_btn') // 加ID,测试好用
// 测试代码
let btn = await driver.findComponent(ON.id('submit_btn'));
坑3:坐标计算依赖屏幕尺寸
不同设备屏幕尺寸不一样,写死坐标会在别的设备上跑不通。
解决方案:用getBounds()动态获取组件位置,或者用相对坐标。
typescript
let bounds = await component.getBounds();
let centerX = bounds.centerX;
let centerY = bounds.centerY;
坑4:手势冲突场景测不准
地图手势和面板手势同时存在时,测试代码模拟的拖拽可能被系统识别成别的操作。
解决方案:测试时把无关手势临时禁用,或者用更精确的坐标控制。
总结
鸿蒙的UiTest框架用起来不算复杂,核心就是三件事:
| 步骤 | 怎么做 |
|---|---|
| 找控件 | driver.findComponent(ON.id('xxx')) |
| 操作控件 | component.click() / component.inputText() / driver.swipe() |
| 验证结果 | expect(actual).assertEqual(expected) |
我们的经验是:核心功能一定要有测试覆盖。比如拖拽面板吸附、路线点拖拽、地图手势这些容易出问题的场景,写测试用例花的半小时,可能帮你省下后面好几个小时的调试时间。
回归测试不再靠人肉。每次改完代码,跑一遍测试用例,几分钟就知道有没有破坏现有功能。
问题发现更早。之前往往是用户反馈了才知道有bug,现在测试用例跑不过就能发现。
重构敢下手了。之前有些代码写得烂但不敢动,怕改坏了。现在有测试兜底,敢改了。
当然,写测试也有成本。一个测试用例少则几十行,多则上百行。但长远来看,在AI辅助的情况下,省下的调试时间远超写测试的时间。
相关资源 : TestKit API参考