HarmonyOS掌上记账APP开发实践第8篇:构建可测试的鸿蒙应用 — MoneyTrack 的自动化测试体系

构建可测试的鸿蒙应用 --- MoneyTrack 的自动化测试体系

概述

在软件开发过程中,测试是保障应用质量、防止回归缺陷和支撑快速迭代的关键环节。然而,移动应用开发中的测试面临诸多挑战:模拟器与真机环境的差异、UI 交互的复杂性、异步操作的时序问题,以及多设备适配带来的测试量激增。HarmonyOS 提供了基于 hypium 测试框架的完整测试体系,支持本地单元测试(Local Unit Test)和仪器化测试(Instrumentation Test)两种模式,分别适用于不同粒度和不同运行环境的测试需求。

MoneyTrack 项目作为一个具备 21 个 HAR 模块的中型应用,在多个模块中建立了规范化的测试目录结构,覆盖了从底层工具函数到上层 Ability 生命周期的测试场景。本文将从测试目录结构、hypium 框架 API 使用、本地测试与仪器化测试的选择策略、Mock 技术应用以及持续集成等角度,全面分析 MoneyTrack 的测试体系设计与最佳实践。

核心知识点

1. 测试目录结构与分类

每个 HAR 模块包含两套测试目录,分别对应两种测试类型:

  • src/test/:本地单元测试(Local Unit Test),在开发机上运行,不依赖真机或模拟器,执行速度快,适合测试纯逻辑代码。
  • src/ohosTest/:仪器化测试(Instrumentation Test),包括 UI 测试和集成测试,需在真机或模拟器上运行,适合测试依赖系统 API 或组件生命周期的场景。

commonlib 模块为例,其测试目录结构如下:

复制代码
commons/commonlib/
├── src/test/
│   ├── List.test.ets          # 测试入口,注册所有测试套件
│   └── LocalUnit.test.ets     # 本地单元测试用例
└── src/ohosTest/
    └── ets/test/
        ├── List.test.ets      # 仪器化测试入口
        └── Ability.test.ets   # Ability 测试用例

MoneyTrack 的 21 个模块中,绝大多数模块都遵循这一目录规范,形成了统一的测试组织方式。

测试体系架构图

以下 mermaid 图展示了 MoneyTrack 的测试分层架构:
#mermaid-svg-wqEcMEuqHf4VUw1h{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wqEcMEuqHf4VUw1h .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wqEcMEuqHf4VUw1h .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wqEcMEuqHf4VUw1h .error-icon{fill:#552222;}#mermaid-svg-wqEcMEuqHf4VUw1h .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wqEcMEuqHf4VUw1h .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wqEcMEuqHf4VUw1h .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wqEcMEuqHf4VUw1h .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wqEcMEuqHf4VUw1h .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wqEcMEuqHf4VUw1h .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wqEcMEuqHf4VUw1h .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wqEcMEuqHf4VUw1h .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wqEcMEuqHf4VUw1h .marker.cross{stroke:#333333;}#mermaid-svg-wqEcMEuqHf4VUw1h svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wqEcMEuqHf4VUw1h p{margin:0;}#mermaid-svg-wqEcMEuqHf4VUw1h .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wqEcMEuqHf4VUw1h .cluster-label text{fill:#333;}#mermaid-svg-wqEcMEuqHf4VUw1h .cluster-label span{color:#333;}#mermaid-svg-wqEcMEuqHf4VUw1h .cluster-label span p{background-color:transparent;}#mermaid-svg-wqEcMEuqHf4VUw1h .label text,#mermaid-svg-wqEcMEuqHf4VUw1h span{fill:#333;color:#333;}#mermaid-svg-wqEcMEuqHf4VUw1h .node rect,#mermaid-svg-wqEcMEuqHf4VUw1h .node circle,#mermaid-svg-wqEcMEuqHf4VUw1h .node ellipse,#mermaid-svg-wqEcMEuqHf4VUw1h .node polygon,#mermaid-svg-wqEcMEuqHf4VUw1h .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wqEcMEuqHf4VUw1h .rough-node .label text,#mermaid-svg-wqEcMEuqHf4VUw1h .node .label text,#mermaid-svg-wqEcMEuqHf4VUw1h .image-shape .label,#mermaid-svg-wqEcMEuqHf4VUw1h .icon-shape .label{text-anchor:middle;}#mermaid-svg-wqEcMEuqHf4VUw1h .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wqEcMEuqHf4VUw1h .rough-node .label,#mermaid-svg-wqEcMEuqHf4VUw1h .node .label,#mermaid-svg-wqEcMEuqHf4VUw1h .image-shape .label,#mermaid-svg-wqEcMEuqHf4VUw1h .icon-shape .label{text-align:center;}#mermaid-svg-wqEcMEuqHf4VUw1h .node.clickable{cursor:pointer;}#mermaid-svg-wqEcMEuqHf4VUw1h .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wqEcMEuqHf4VUw1h .arrowheadPath{fill:#333333;}#mermaid-svg-wqEcMEuqHf4VUw1h .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wqEcMEuqHf4VUw1h .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wqEcMEuqHf4VUw1h .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wqEcMEuqHf4VUw1h .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wqEcMEuqHf4VUw1h .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wqEcMEuqHf4VUw1h .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wqEcMEuqHf4VUw1h .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wqEcMEuqHf4VUw1h .cluster text{fill:#333;}#mermaid-svg-wqEcMEuqHf4VUw1h .cluster span{color:#333;}#mermaid-svg-wqEcMEuqHf4VUw1h div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wqEcMEuqHf4VUw1h .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wqEcMEuqHf4VUw1h rect.text{fill:none;stroke-width:0;}#mermaid-svg-wqEcMEuqHf4VUw1h .icon-shape,#mermaid-svg-wqEcMEuqHf4VUw1h .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wqEcMEuqHf4VUw1h .icon-shape p,#mermaid-svg-wqEcMEuqHf4VUw1h .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wqEcMEuqHf4VUw1h .icon-shape .label rect,#mermaid-svg-wqEcMEuqHf4VUw1h .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wqEcMEuqHf4VUw1h .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wqEcMEuqHf4VUw1h .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wqEcMEuqHf4VUw1h :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 测试配置
MoneyTrack 测试体系
本地单元测试

src/test/
仪器化测试

src/ohosTest/
纯逻辑函数测试
数据模型测试
工具类测试
无 UI 依赖的服务测试
Ability 生命周期测试
UI 组件测试
模块集成测试
系统 API 交互测试
执行环境: 开发机
执行环境: 真机/模拟器
List.test.ets 注册
ohosTest/module.json5

2. hypium 测试框架

hypium 是鸿蒙官方测试框架,提供 BDD(Behavior-Driven Development)风格的测试 API,包括 describeitexpect 以及完整的生命周期钩子函数。

生命周期钩子

MoneyTrack 中的每个测试文件都完整使用了 hypium 的生命周期钩子,以实现测试环境的精确控制:

typescript 复制代码
import {
  describe,
  beforeAll,
  beforeEach,
  afterEach,
  afterAll,
  it,
  expect,
} from '@ohos/hypium';

export default function abilityTest() {
  describe('ActsAbilityTest', () => {
    // 在所有测试用例执行前执行一次
    beforeAll(() => {
      // 适合放置全局初始化逻辑:创建数据库、初始化全局状态等
    });

    // 在每个测试用例执行前执行
    beforeEach(() => {
      // 适合放置每个用例的预设操作:重置测试数据、清理状态等
    });

    // 在每个测试用例执行后执行
    afterEach(() => {
      // 适合放置每个用例的清理操作:释放临时资源、重置 Mock 等
    });

    // 在所有测试用例执行后执行一次
    afterAll(() => {
      // 适合放置全局清理逻辑:关闭数据库连接、释放全局资源等
    });

    it('assertContain', 0, () => {
      let a = 'abc';
      let b = 'b';
      expect(a).assertContain(b);
      expect(a).assertEqual(a);
    });
  });
}

各生命周期钩子的执行顺序为:beforeAllbeforeEachitafterEach(重复前三步)→ afterAll

断言语法的使用

hypium 的 expect 模块提供了丰富的断言方法,适用于不同的验证场景:

typescript 复制代码
// 字符串包含断言
expect(a).assertContain(b);

// 严格相等断言
expect(a).assertEqual(a);

// 真值断言
expect(isReady).assertTrue();

// 未定义断言
expect(result).assertUndefined();

// 实例类型断言
expect(obj).assertInstanceOf('Array');

3. 本地测试与仪器化测试的选择建议

在实际项目中,合理选择测试类型至关重要:

测试类型 运行环境 执行速度 适用场景
本地单元测试 开发机(JVM) 快(毫秒级) 纯逻辑函数、数据模型、工具类、无 UI 依赖的服务
仪器化测试 真机/模拟器 慢(秒级) Ability 生命周期、UI 交互、系统 API 调用、跨模块集成

选择建议

  1. 优先编写本地单元测试:对于不依赖系统 API 的纯逻辑代码(如数据格式化、金额计算、日期处理等),优先使用本地单元测试。它们执行速度快,可以在每次代码提交时完整运行。

  2. 将仪器化测试聚焦于关键场景:仪器化测试由于需要在真机上运行,执行速度较慢,应聚焦于核心业务流程(如页面跳转、数据持久化、Ability 生命周期)和需要系统 API 支持的功能(如推送通知、传感器数据)。

  3. 合理拆分:一个模块两套测试 :MoneyTrack 的做法是每个模块同时保留 src/test/src/ohosTest/,将纯逻辑测试放在本地测试中,将 UI 和集成测试放在仪器化测试中,既保证了覆盖率,又控制了测试执行时间。

4. 测试模块的 module.json5 配置

仪器化测试模块需要在 src/ohosTest/module.json5 中定义独立的模块配置,type 通常为 "feature"

json 复制代码
{
  "module": {
    "name": "commonlib_test",
    "type": "feature",
    "deviceTypes": [
      "default",
      "tablet"
    ],
    "deliveryWithInstall": true,
    "installationFree": false
  }
}

在 MoneyTrack 中,几乎所有功能模块(homeminestatisticsassets 等)和公共组件模块(bill_cardasset_managemembership 等)都包含了 ohosTest/module.json5 配置文件,这表明项目对仪器化测试的重视。

5. Mock 与 Stub 在鸿蒙测试中的应用

在鸿蒙应用测试中,Mock 和 Stub 是处理外部依赖的重要技术:

  • Mock:模拟外部对象的行为,验证其是否按预期被调用。适用于需要验证交互场景的测试。
  • Stub:提供预设的返回值,替代真实的外部依赖。适用于需要隔离被测单元的测试。

在 MoneyTrack 的测试场景中,常见的 Mock 应用包括:

  1. Mock 网络请求:在测试数据层代码时,使用 Mock 替代真实的 HTTP 请求,避免测试对外部网络的依赖,同时可以模拟各种异常场景(超时、网络错误、错误状态码)。

  2. Mock 数据库操作:在测试业务逻辑时,使用 Mock 替代真实的数据库读写操作,确保测试的隔离性和可重复性。

  3. Stub 系统能力 :对于依赖系统 API(如 @ohos.data.preferences@ohos.ability.wantConstant)的代码,使用 Stub 提供预设的返回值,使测试不依赖真机环境。

Mock 测试的伪代码示例:

typescript 复制代码
// Mock 网络请求的典型模式
import { describe, it, expect } from '@ohos/hypium';

describe('dataServiceTest', () => {
  it('should handle network error gracefully', 0, () => {
    // 使用 Stub 替代真实网络请求,返回模拟的错误响应
    const mockResponse = { code: 500, message: 'Internal Server Error' };
    // stubNetworkRequest(mockResponse);
    
    // 执行被测方法
    // const result = dataService.fetchData();
    
    // 验证错误处理逻辑
    // expect(result).assertEqual(defaultFallbackValue);
  });
});

6. 测试覆盖率与持续集成建议

测试覆盖率

为了确保测试的有效性,团队应关注以下覆盖率指标:

  1. 行覆盖率(Line Coverage):至少达到 70% 以上,确保核心逻辑路径被执行到。
  2. 分支覆盖率(Branch Coverage) :关注条件分支的覆盖情况,尤其是 if/elseswitch/case 语句。
  3. 方法覆盖率(Method Coverage):至少覆盖 80% 的公开方法。

建议在项目持续集成(CI)流程中集成覆盖率工具,对每次提交的覆盖率变化进行监控,并设置覆盖率门禁------当新增代码的覆盖率低于阈值时,构建失败。

持续集成流程

将测试集成到 CI 流水线中的推荐做法:

  1. 提交触发本地测试:每次代码提交时,在 CI 环境中运行所有本地单元测试。本地测试执行速度快,可以在几分钟内完成,适合作为提交门禁。

  2. 定时运行仪器化测试:仪器化测试由于需要真机环境,可以在夜间或定时任务中运行,生成详细的测试报告。

  3. 增量覆盖率检查:对比当前分支与目标分支的测试覆盖率变化,确保新代码有对应的测试用例覆盖。

  4. 失败快速反馈:当测试失败时,通过即时通讯工具通知相关负责人,缩短问题发现和修复的周期。

最佳实践与注意事项

  1. 保持测试的独立性 :每个测试用例应该是独立的、可重复执行的,不依赖其他测试用例的执行顺序或状态。利用 beforeEachafterEach 在每个测试前后重置环境,避免测试之间的状态污染。

  2. 统一测试入口注册方式 :在每个模块的 src/test/List.test.etssrc/ohosTest/ets/test/List.test.ets 中注册所有测试套件,保持入口文件的简洁性,只在 List.test.ets 中做导入和注册,具体的测试逻辑写在独立的 *.test.ets 文件中。

  3. 测试用例的命名规范 :使用清晰的三段式命名------"被测方法_场景_预期结果",例如 fetchData_networkError_returnsDefaultValue,便于快速定位失败的测试用例。

  4. 合理划分测试粒度:避免编写过于粗粒度(一次测试覆盖太多逻辑)或过于细粒度(一个函数拆成十几个测试)的测试用例。推荐一个核心函数对应 3-5 个测试用例,覆盖正常路径、边界条件和异常场景。

  5. 善用 hypium 的生命周期钩子beforeAll 适合全局初始化(如创建数据库连接池),beforeEach 适合用例级准备(如插入测试数据),afterEach 适合用例级清理(如删除测试数据),afterAll 适合全局清理(如关闭连接)。合理利用这些钩子可以显著减少重复代码。

总结

MoneyTrack 的测试体系通过 hypium 测试框架建立了一套规范化的分层测试架构。本地单元测试(src/test/)覆盖纯逻辑和工具类函数,提供快速反馈;仪器化测试(src/ohosTest/``)覆盖 Ability 生命周期和 UI 交互,保障核心流程的正确性。每个模块的双测试目录结构、统一的 List.test.ets注册机制和完善的module.json5` 配置,共同构成了一个可维护、可扩展的测试基础设施。结合 Mock/Stub 技术处理外部依赖、覆盖率指标监控测试质量、以及持续集成流程自动化测试执行,MoneyTrack 的测试体系为应用质量提供了坚实的保障。在后续开发中,随着测试用例的不断丰富,这一体系将持续支撑项目的稳健演进。

参考文档

  • Test Kit 测试框架指南
  • hypium 测试框架 API 参考
  • 本地单元测试与仪器化测试最佳实践
相关推荐
梦想不只是梦与想4 小时前
鸿蒙中declare关键字
harmonyos·declare
FrameNotWork5 小时前
HarmonyOS 6.0 输入法与键盘避让 — 聊天页面底部输入栏被键盘挡住的绝望
华为·计算机外设·harmonyos
念雨思5 小时前
信用卡权益对比:基于HarmonyOS + ArkTS的AI智能金融助手开发实践
人工智能·学习·华为·金融·harmonyos·鸿蒙
不羁的木木6 小时前
HarmonyOS APP实战-基于Image Kit的图像处理APP - 第12篇:性能优化与内存管理
图像处理·性能优化·harmonyos
拥抱太阳06166 小时前
HarmonyOS 应用开发《掌上英语》第6篇-资源管理最佳实践多模块资源复用
pytorch·深度学习·harmonyos
2301_768103497 小时前
HarmonyOS趣味相机实战第7篇:Preferences相册缓存、旧数据兼容与异常兜底
harmonyos·arkts·preferences·本地缓存·数据兼容
HONG````7 小时前
ArkTS 并发编程:TaskPool 多线程实战
harmonyos·arkts
不羁的木木7 小时前
HarmonyOS APP实战-基于Image Kit的图像处理APP - 第8篇:图片编码与保存
图像处理·华为·harmonyos