鸿蒙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之后的版本。

  1. 创建测试目录:在模块下创建src/test/ets/目录
  2. 依赖导入:在测试文件中导入核心依赖
  3. 测试运行器:使用项目自带的测试运行器,或命令行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辅助编程工具,支持自动生成单元测试用例:

  1. 将光标置于方法名称上,或框选完整的待测试方法代码块
  2. 右键选择 CodeGenie > Generate UT
  3. AI会自动分析代码并生成对应的单元测试用例
  4. 生成的测试文件默认保存在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中执行测试脚本的三种方式:

  1. 测试包级别执行:执行测试包内全部用例
  2. 测试套级别执行:执行describe方法中定义的全部测试用例
  3. 测试方法级别执行:执行指定it方法(单条用例)

执行完成后,可直接在IDE中查看测试结果,包括通过/失败统计、失败堆栈等。

四、华为云测平台接入流程

4.1 云测试简介

华为AppGallery Connect云测试提供了一站式应用测试服务,可自动化完成兼容性测试、稳定性测试、性能测试、功耗测试、UX测试和安全测试,快速出具专业详细的测试报告,帮助提前发现并精准定位问题。

核心价值

  • 打破设备依赖:无需购买大量真机设备
  • 自动化执行:7×24小时自动测试
  • 详细报告:问题截图、日志、性能数据
  • 免费额度:每个开发者账号每天300分钟免费额度

4.2 准备工作

在接入云测试前,需要完成以下准备:

4.2.1 证书与包配置
  1. 配置发布证书:使用发布证书签名应用
  2. 编译模式:选择release模式打包
  3. 应用包格式:.hap或.app格式,大小不超过4GB
  4. 包名与版本号:可在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 测试原则与建议

  1. 测试金字塔原则:70%单元测试 + 20%集成测试 + 10%UI测试
  2. 可测试性设计:组件设计时考虑测试(可访问性文本、ID)
  3. 稳定性优先:合理使用等待机制,避免硬编码延时
  4. 数据隔离:测试数据与生产数据隔离,使用Mock
  5. 持续反馈:测试结果及时通知,快速修复

6.3 避坑指南

坑点 表现 解决方案
UI测试不稳定 偶发性失败 增加显式等待,避免sleep
测试数据污染 测试间相互影响 每个用例独立准备/清理数据
忽略边界测试 线上出现边界异常 数据驱动测试,覆盖边界值
过度依赖云测 成本高、反馈慢 本地自动化+云测验证结合
测试代码难维护 业务变化测试失效 封装Page Object模式,减少硬编码

6.4 未来的测试趋势

随着鸿蒙生态的发展,测试工具也在持续进化:

  • AI模糊测试:基于模型生成异常输入,自动挖掘潜在崩溃
  • 全链路追踪:整合分布式调试器,实现跨设备调用链可视化
  • 智能用例推荐:基于代码变更分析推荐高优先级测试项
  • 云测真机集群:提供云端真实设备矩阵,一键发起多机型兼容性测试

结语:测试是开发者的护身符

写到这里,我想跟你聊点大实话:

国内很多鸿蒙项目的最大问题不是技术门槛,而是缺乏测试文化。大家都觉得写测试浪费时间、测试不产生功能、上线前改一下就行了。

但实际情况是:不写测试,浪费的不是开发时间,而是整个项目的生命。UI改了一处导致多个页面崩溃,表单动了一下把支付流程干断,新人加了一个条件判断直接把主链路打碎——这些问题,本可以用一条简单的单元测试避免。

测试不是为了让项目更快上线,而是让项目“不再反复返工”。

从今天开始,为你正在开发的鸿蒙应用补上第一行测试代码。无论是工具函数的单元测试,还是核心按钮的UI自动化,亦或是上架前的云测试验证——每多一分严谨,用户就少一分抱怨。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐