鸿蒙APP自动化测试体系构建:从单元测试到云测
UI测试框架主要对外提供UiTest API,用于模拟用户操作并验证UI响应。其脚本运行基于单元测试框架。核心能力组件查找(通过ID、文本、类型等)事件模拟(点击、滑动、输入)状态断言(存在、文本、属性)约束与限制UI测试框架能力在HarmonyOS 3.0 release版本之后方可使用只支持应用内使用,不支持与权限弹窗、SystemUI控件交互。
鸿蒙APP自动化测试体系构建:从单元测试到云测
测试——从“可选”到“必修”
在鸿蒙应用开发社区里,流传着这样一句“大实话”:没有测试的应用,就像没有安全网的高空走钢丝——刺激归刺激,掉下去一次就够你喝一壶。
很多开发者有这样的习惯:功能写完了→自己点几下→哎挺稳→打包上线→用户骂来了。然后开始反思:“怎么我这逻辑昨天还好好的?”“为啥用户点到第3屏就卡死?”“UI事件链路我不是测过了吗?”——其实你并没有测试,你只是“浏览了一遍”。
随着鸿蒙生态的快速发展,应用复杂度持续攀升,单靠手工点按已无法保障质量。华为官方数据显示,通过自动化测试提前发现并修复缺陷,可将应用上架后的崩溃率降低70%以上。本文将从单元测试、UI自动化测试、云测试三个维度,系统讲解鸿蒙APP自动化测试体系的构建方法,帮助开发者打造高质量的应用。
一、鸿蒙测试体系全景图
1.1 测试分层模型
在鸿蒙应用开发中,典型的测试活动模型分为五个层次:
| 测试层级 | 测试对象 | 核心目标 | 推荐工具 |
|---|---|---|---|
| 单元测试 | 函数、类、组件 | 保障代码逻辑正确,异常处理充分 | JsUnit(arkxtest) |
| 集成测试 | 多个类/组件组合 | 验证接口正确、组件完整 | JsUnit + Mock |
| UI测试 | 应用界面、交互 | 验证功能正确实现,用户场景可达 | UiTest框架 |
| 体验测试 | 全应用 | 兼容性、稳定性、性能、功耗、UX | DevEco Testing / 云测试 |
| 用户测试 | 真实用户 | 感知卓越、好用、爱用 | 内部测试/邀请测试/公开测试 |
1.2 测试工具矩阵
鸿蒙生态提供了完整的测试工具链:
| 工具名称 | 适用场景 | 核心能力 |
|---|---|---|
| JsUnit | 单元/集成测试 | 基于Jasmine的BDD风格API,支持断言、Mock、覆盖率 |
| UiTest | UI自动化测试 | 组件查找、事件模拟、状态断言 |
| PerfTest | 性能测试 | 内存、CPU、帧率等性能指标采集 |
| DevEco Testing | 专项测试 | 兼容性、稳定性、安全、功耗测试,支持服务卡片式操作 |
| 云测试 | 大规模真机测试 | 海量远程真机,自动化执行,详细报告 |
1.3 测试金字塔原则
对于鸿蒙项目,建议遵循测试金字塔原则:
- 70% 单元测试:业务逻辑、工具函数、状态管理
- 20% 集成测试:服务调用、组件组合
- 10% UI自动化测试:核心用户旅程、端到端场景
二、ArkTS单元测试框架使用指南
2.1 单元测试框架:JsUnit(arkxtest)
鸿蒙的单元测试框架arkxtest提供了基于Jasmine的BDD风格API,支持测试用例识别、调度执行和结果汇总。
2.1.1 环境搭建
自动化脚本的编写主要基于DevEco Studio,建议使用3.1.0.400之后的版本。
- 创建测试目录:在模块下创建
src/test/ets/目录 - 依赖导入:在测试文件中导入核心依赖
- 测试运行器:使用项目自带的测试运行器,或命令行
ohpm test
2.1.2 第一个单元测试
以一个简单的计算器函数为例:
// src/main/ets/utils/calculator.ts
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('除数不能为0');
}
return a / b;
}
对应的单元测试:
// src/test/ets/utils/calculator.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { add, subtract, multiply, divide } from '../../../main/ets/utils/calculator';
describe('calculator', () => {
it('add should return correct sum', () => {
expect(add(2, 3)).assertEqual(5);
expect(add(-1, 1)).assertEqual(0);
expect(add(0, 0)).assertEqual(0);
});
it('subtract should return correct difference', () => {
expect(subtract(5, 3)).assertEqual(2);
expect(subtract(1, 5)).assertEqual(-4);
});
it('multiply should return correct product', () => {
expect(multiply(2, 3)).assertEqual(6);
expect(multiply(-2, 3)).assertEqual(-6);
expect(multiply(0, 5)).assertEqual(0);
});
it('divide should return correct quotient', () => {
expect(divide(6, 3)).assertEqual(2);
expect(divide(5, 2)).assertEqual(2.5);
});
it('divide should throw error when dividing by zero', () => {
expect(() => divide(5, 0)).assertThrowError('除数不能为0');
});
});
2.2 测试组件状态与行为
单元测试不仅可测试纯函数,还能测试组件的状态变化和事件响应。
2.2.1 待测组件示例
// src/main/ets/components/Counter.ets
@Component
export struct Counter {
@State private value: number = 0;
@Prop step: number = 1;
onChange?: (newValue: number) => void;
build() {
Column({ space: 8 }) {
Text(`计数:${this.value}`)
.id('counterText')
.fontSize(20)
Button(`+${this.step}`)
.id('addBtn')
.onClick(() => {
this.value += this.step;
this.onChange?.(this.value);
})
}
.padding(12)
}
}
2.2.2 组件驱动器模式
为了不依赖真实UI渲染就能测试组件逻辑,可以设计一个“组件驱动器”:
// src/test/ets/__helpers__/ComponentDriver.ets
export class ComponentDriver<T> {
private instance: T;
constructor(factory: () => T) {
this.instance = factory();
// 触发生命周期(如果存在)
if (typeof (this.instance as any).aboutToAppear === 'function') {
(this.instance as any).aboutToAppear();
}
}
get(): T {
return this.instance;
}
// 针对Counter的专用操作方法
clickPlus() {
const inst: any = this.instance;
inst.value += inst.step;
inst.onChange?.(inst.value);
}
getValue(): number {
return (this.instance as any).value;
}
}
2.2.3 组件单元测试
// src/test/ets/components/Counter.spec.ets
import { describe, it, expect, beforeEach } from '@ohos/hypium';
import { Counter } from '../../../main/ets/components/Counter';
import { ComponentDriver } from '../__helpers__/ComponentDriver';
describe('Counter组件测试', () => {
let changedValues: number[] = [];
let driver: ComponentDriver<Counter>;
beforeEach(() => {
changedValues = [];
driver = new ComponentDriver(() => {
const counter = new Counter();
counter.step = 2; // 设置步长
counter.onChange = (val) => changedValues.push(val);
return counter;
});
});
it('初始值应为0', () => {
expect(driver.getValue()).assertEqual(0);
});
it('点击应按照步长增加并触发回调', () => {
driver.clickPlus();
expect(driver.getValue()).assertEqual(2);
expect(changedValues.length).assertEqual(1);
expect(changedValues[0]).assertEqual(2);
});
it('多次点击应累加', () => {
driver.clickPlus();
driver.clickPlus();
driver.clickPlus();
expect(driver.getValue()).assertEqual(6);
expect(changedValues).assertDeepEquals([2, 4, 6]);
});
});
测试要点:
- 单测无需渲染真实UI,只要能驱动状态并验证回调即可
- 把“交互”抽象成driver,重构组件内部结构时测试不易碎
- 用例命名清晰表达意图,覆盖边界值
2.3 Mock与异步测试
2.3.1 Mock网络请求
// src/test/ets/services/userService.test.ets
import { describe, it, expect, beforeEach, afterEach } from '@ohos/hypium';
import { UserService } from '../../../main/ets/services/UserService';
// Mock http模块
jest.mock('@ohos.net.http', () => ({
createHttp: () => ({
request: jest.fn().mockResolvedValue({
responseCode: 200,
result: JSON.stringify({ id: 1, name: '张三' })
})
})
}), { virtual: true });
describe('UserService', () => {
let userService: UserService;
beforeEach(() => {
userService = new UserService();
});
it('getUserById应返回用户数据', async () => {
const user = await userService.getUserById(1);
expect(user.id).assertEqual(1);
expect(user.name).assertEqual('张三');
});
});
2.3.2 异步测试
it('异步操作测试', 0, async function (done) {
// 执行异步操作
const result = await someAsyncFunction();
// 断言结果
expect(result).assertEqual('expected');
// 调用done表示异步完成
done();
});
2.4 测试覆盖率
在DevEco Studio中,可以配置测试覆盖率收集:
// oh-package.json5
{
"name": "entry",
"version": "1.0.0",
"testOptions": {
"coverage": {
"enable": true,
"includes": ["src/main/ets/**/*.ets"],
"excludes": ["src/main/ets/mock/**"]
}
}
}
执行测试后,可在build/reports/coverage目录查看覆盖率报告。
2.5 智能生成单元测试
DevEco Studio集成了CodeGenie AI辅助编程工具,支持自动生成单元测试用例:
- 将光标置于方法名称上,或框选完整的待测试方法代码块
- 右键选择 CodeGenie > Generate UT
- AI会自动分析代码并生成对应的单元测试用例
- 生成的测试文件默认保存在
ohosTest/ets/test目录
约束条件:
- 最多支持解读30000字符以内的代码片段
- ArkUI代码、生命周期函数、@Extend/@Styles/@Builder修饰的函数、private修饰的私有函数不支持生成
三、UI自动化测试(UiTest)脚本编写
3.1 UiTest框架概述
UI测试框架主要对外提供UiTest API,用于模拟用户操作并验证UI响应。其脚本运行基于单元测试框架。
核心能力:
- 组件查找(通过ID、文本、类型等)
- 事件模拟(点击、滑动、输入)
- 状态断言(存在、文本、属性)
约束与限制:
- UI测试框架能力在HarmonyOS 3.0 release版本之后方可使用
- 只支持应用内使用,不支持与权限弹窗、SystemUI控件交互
3.2 环境配置与依赖
在测试文件中导入必要的依赖:
import { describe, beforeAll, it, expect } from '@ohos/hypium';
import abilityDelegatorRegistry from '@ohos.application.abilityDelegatorRegistry';
import { Driver, ON, Component, MatchPattern } from '@ohos.UiTest';
获取AbilityDelegator用于启动被测Ability:
const delegator = abilityDelegatorRegistry.getAbilityDelegator();
3.3 基础UI测试脚本
以下示例演示如何启动应用,查找按钮并点击,然后验证UI变化:
// src/ohosTest/ets/test/BasicUiTest.test.ets
import { describe, it, expect } from '@ohos/hypium';
import abilityDelegatorRegistry from '@ohos.application.abilityDelegatorRegistry';
import { Driver, ON } from '@ohos.UiTest';
const delegator = abilityDelegatorRegistry.getAbilityDelegator();
export default function basicUiTest() {
describe('基础UI测试示例', () => {
it('点击按钮更新计数', 0, async function (done) {
console.info('UI测试开始');
// 1. 启动被测Ability
await delegator.executeShellCommand('aa start -b com.example.myapp -a MainAbility')
.then(() => console.info('启动成功'))
.catch(err => console.info('启动失败: ' + err));
// 等待应用启动
await sleep(2000);
// 2. 创建Driver实例
let driver = await Driver.create();
// 3. 查找按钮组件(通过ID)
let button = await driver.findComponent(ON.id('addBtn'));
// 4. 点击按钮
await button.click();
await driver.delayMs(500);
// 5. 查找文本组件并验证
let textComponent = await driver.findComponent(ON.id('counterText'));
let textValue = await textComponent.getText();
// 断言文本内容
expect(textValue).assertEqual('计数:1');
// 6. 返回退出
await driver.pressBack();
done();
});
});
}
function sleep(time: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, time));
}
3.4 组件查找策略
UiTest框架提供了多种组件查找方式:
| 查找方式 | API | 示例 |
|---|---|---|
| 通过ID | ON.id('btnId') |
ON.id('addBtn') |
| 通过文本 | ON.text('登录') |
ON.text('Next') |
| 通过文本包含 | ON.textContains('计') |
ON.textContains('计数') |
| 通过类型 | ON.type(Button) |
ON.type(Button) |
| 组合条件 | ON.id('btn').text('登录') |
ON.id('btn').text('登录') |
查找建议:优先使用可访问性文本(accessibilityText)作为定位依据,这对自动化测试和无障碍都友好。
3.5 高级UI测试场景
3.5.1 表单输入与提交
it('登录表单测试', async function (done) {
let driver = await Driver.create();
// 查找输入框
let usernameInput = await driver.findComponent(ON.id('usernameInput'));
let passwordInput = await driver.findComponent(ON.id('passwordInput'));
let loginBtn = await driver.findComponent(ON.id('loginBtn'));
// 输入内容
await usernameInput.inputText('testuser');
await passwordInput.inputText('123456');
// 点击登录
await loginBtn.click();
await driver.delayMs(2000);
// 验证登录成功(检查欢迎文本)
let welcomeText = await driver.findComponent(ON.textContains('欢迎'));
let exists = await welcomeText.isExist();
expect(exists).assertTrue();
done();
});
3.5.2 列表滚动与查找
it('列表滚动后查找元素', async function (done) {
let driver = await Driver.create();
// 查找列表组件
let list = await driver.findComponent(ON.id('todoList'));
// 滚动到包含指定文本的项
await list.scrollTo(ON.text('待办项50'));
// 查找该项
let targetItem = await driver.findComponent(ON.text('待办项50'));
let exists = await targetItem.isExist();
expect(exists).assertTrue();
done();
});
3.5.3 滑动操作
it('滑动轮播图', async function (done) {
let driver = await Driver.create();
let swiper = await driver.findComponent(ON.id('bannerSwiper'));
// 向左滑动
await swiper.swipeLeft();
await driver.delayMs(1000);
// 向右滑动
await swiper.swipeRight();
done();
});
3.6 等待机制与稳定性优化
UI自动化测试的一大挑战是稳定性。以下技巧可提升测试稳定性:
3.6.1 显式等待
async function waitForElement(driver: Driver, selector: any, timeout: number = 5000): Promise<Component> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
let component = await driver.findComponent(selector);
let exists = await component.isExist();
if (exists) {
return component;
}
} catch (e) {
// 忽略查找错误
}
await driver.delayMs(500);
}
throw new Error(`等待元素超时: ${JSON.stringify(selector)}`);
}
3.6.2 条件等待
// 等待文本变化
it('等待文本更新', async function (done) {
let driver = await Driver.create();
let textComp = await driver.findComponent(ON.id('statusText'));
let initialText = await textComp.getText();
// 点击触发更新的按钮
let updateBtn = await driver.findComponent(ON.id('updateBtn'));
await updateBtn.click();
// 等待文本变化
await driver.delayMs(1000);
let newText = await textComp.getText();
expect(initialText !== newText).assertTrue();
done();
});
3.7 测试执行与结果查看
在DevEco Studio中执行测试脚本的三种方式:
- 测试包级别执行:执行测试包内全部用例
- 测试套级别执行:执行describe方法中定义的全部测试用例
- 测试方法级别执行:执行指定it方法(单条用例)
执行完成后,可直接在IDE中查看测试结果,包括通过/失败统计、失败堆栈等。
四、华为云测平台接入流程
4.1 云测试简介
华为AppGallery Connect云测试提供了一站式应用测试服务,可自动化完成兼容性测试、稳定性测试、性能测试、功耗测试、UX测试和安全测试,快速出具专业详细的测试报告,帮助提前发现并精准定位问题。
核心价值:
- 打破设备依赖:无需购买大量真机设备
- 自动化执行:7×24小时自动测试
- 详细报告:问题截图、日志、性能数据
- 免费额度:每个开发者账号每天300分钟免费额度
4.2 准备工作
在接入云测试前,需要完成以下准备:
4.2.1 证书与包配置
- 配置发布证书:使用发布证书签名应用
- 编译模式:选择release模式打包
- 应用包格式:.hap或.app格式,大小不超过4GB
- 包名与版本号:可在
app.json5中查看确认
4.2.2 隐私配置(如需隐私测试)
- 应用已配置隐私声明和隐私标签
- 待检测应用包关联的应用在当前账号下
4.3 云测试接入流程
步骤1:登录AGC平台
访问AppGallery Connect,点击“开发与服务”。
步骤2:进入云测试
在项目列表中点击需要测试的项目,在左侧导航栏选择“质量 > 云测试”。
步骤3:创建测试任务
点击右上角“创建测试”,进入创建测试任务页面。
配置项说明:
| 配置项 | 说明 |
|---|---|
| 测试任务名称 | 自定义,最长64字符(中文/字母/数字/下划线) |
| 应用程序 | 本地上传已签名的release包,或选择历史包 |
| 测试场景 | 上架测试 / 自定义测试 |
| 测试范围 | 兼容性、稳定性、性能、功耗、UX、隐私测试 |
| 应用分类 | 根据实际分类选择(三级展示) |
| 预设内容 | 如需账号密码登录,可预设登录信息 |
上传限制:每个开发者账号每天最多上传500次(成功/失败均计数)。
步骤4:选择测试设备
- 选择待测机型(每次仅支持选择1台)
- 标有“惠”字的设备可使用免费额度
- HarmonyOS NEXT设备数量有限,建议8:00~12:00错峰提交
步骤5:提交测试
确认所选设备数和预估时长后,点击“提交”。
步骤6:查看测试报告
测试完成后,系统会发送邮件通知。在云测试首页可查看报告。
4.4 测试报告解读
4.4.1 报告概览
上架测试报告包含以下信息:
- 应用信息:名称、版本、API Level、大小
- 测试专项:兼容性、稳定性、性能、功耗、UX、隐私测试
- 测试结果:各专项的通过率、问题分布
4.4.2 各专项报告详解
兼容性测试:
- 检测应用在真机设备上的兼容性问题
- 包括安装失败、启动失败、界面显示异常等
稳定性测试:
- 检测应用在长时间运行中的稳定性
- 包括崩溃、无响应、内存泄漏等
性能测试:
- 采集CPU、内存、耗电量、流量等关键指标
- 分析应用性能薄弱点
功耗测试:
- 检测影响手机功耗的各项关键指标
- 包括后台耗电、传感器使用等
UX测试:
- 验证基础体验、系统特性适配、视觉风格、动效、大屏体验等
隐私测试:
- 检测隐私声明合规性、权限使用合理性
4.4.3 问题定位
点击具体问题可查看:
- 问题截图:复现问题时的界面截图
- 操作步骤:导致问题的操作序列
- 日志信息:详细的错误日志
- 问题描述:原因、位置、坐标说明
4.5 免费额度与计费
免费额度:
- 每个开发者账号每天300分钟免费测试时长
- 仅适用于标有“惠”字的优惠机型
计费说明:
- 超出免费额度后,可选择订购付费套餐包或开通按量付费
- 实际扣费时长以测试实际时长为准
4.6 常见问题处理
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 上传失败 | 包格式不对/签名错误 | 确认使用release模式+发布证书打包 |
| 无可选机型 | 安装包上传问题 | 刷新页面/重新上传 |
| 测试场景无选项 | 应用类型限制 | 确认应用分类是否正确 |
| 隐私测试不通过 | 隐私声明未配置 | 配置隐私声明和标签 |
五、持续集成中的测试实践
5.1 将测试接入CI/CD
通过DevEco CLI将测试任务嵌入Jenkins/GitLab CI流水线:
# .gitlab-ci.yml
stages:
- build
- test
build:
stage: build
script:
- hvigor clean assembleHap
unit-test:
stage: test
script:
- hvigog run Test --bundle com.example.myapp
artifacts:
reports:
junit: build/reports/test-results.xml
paths:
- build/reports/coverage/
5.2 测试质量门禁
设置质量门禁标准:
- 单元测试通过率:100%
- 代码覆盖率:≥80%
- UI自动化测试通过率:100%
- 性能基线:无退化
5.3 测试左移实践
“测试左移”指在开发早期引入测试活动:
- 编码阶段:IDE实时检查+AI生成单元测试
- 代码审查:测试覆盖率作为审查指标
- 提交阶段:CI自动运行核心测试套件
六、总结与最佳实践
6.1 测试体系构建路线图
| 阶段 | 目标 | 关键动作 |
|---|---|---|
| 起步期 | 核心逻辑有测试 | 对工具函数、Service层编写单元测试 |
| 成长期 | 关键UI有自动化 | 对核心用户旅程编写UI测试 |
| 成熟期 | 全流程自动化 | 接入CI/CD,建立质量门禁 |
| 领先期 | 测试驱动开发 | 测试先行,AI辅助生成 |
6.2 测试原则与建议
- 测试金字塔原则:70%单元测试 + 20%集成测试 + 10%UI测试
- 可测试性设计:组件设计时考虑测试(可访问性文本、ID)
- 稳定性优先:合理使用等待机制,避免硬编码延时
- 数据隔离:测试数据与生产数据隔离,使用Mock
- 持续反馈:测试结果及时通知,快速修复
6.3 避坑指南
| 坑点 | 表现 | 解决方案 |
|---|---|---|
| UI测试不稳定 | 偶发性失败 | 增加显式等待,避免sleep |
| 测试数据污染 | 测试间相互影响 | 每个用例独立准备/清理数据 |
| 忽略边界测试 | 线上出现边界异常 | 数据驱动测试,覆盖边界值 |
| 过度依赖云测 | 成本高、反馈慢 | 本地自动化+云测验证结合 |
| 测试代码难维护 | 业务变化测试失效 | 封装Page Object模式,减少硬编码 |
6.4 未来的测试趋势
随着鸿蒙生态的发展,测试工具也在持续进化:
- AI模糊测试:基于模型生成异常输入,自动挖掘潜在崩溃
- 全链路追踪:整合分布式调试器,实现跨设备调用链可视化
- 智能用例推荐:基于代码变更分析推荐高优先级测试项
- 云测真机集群:提供云端真实设备矩阵,一键发起多机型兼容性测试
结语:测试是开发者的护身符
写到这里,我想跟你聊点大实话:
国内很多鸿蒙项目的最大问题不是技术门槛,而是缺乏测试文化。大家都觉得写测试浪费时间、测试不产生功能、上线前改一下就行了。
但实际情况是:不写测试,浪费的不是开发时间,而是整个项目的生命。UI改了一处导致多个页面崩溃,表单动了一下把支付流程干断,新人加了一个条件判断直接把主链路打碎——这些问题,本可以用一条简单的单元测试避免。
测试不是为了让项目更快上线,而是让项目“不再反复返工”。
从今天开始,为你正在开发的鸿蒙应用补上第一行测试代码。无论是工具函数的单元测试,还是核心按钮的UI自动化,亦或是上架前的云测试验证——每多一分严谨,用户就少一分抱怨。
更多推荐


所有评论(0)