鸿蒙测试框架实战 — 从单元测试到 UI 测试的全栈覆盖

在这里插入图片描述

文章简介

测试是保障代码质量的关键手段。HarmonyOS 提供了一套完整的测试框架 hypium,支持单元测试(LocalUnit)、集成测试(ohosTest)和 UI 测试等不同层次的测试。MoneyTrack 在各个模块中都配置了测试目录,通过 List.test.ets 和 Ability.test.ets 组织测试用例。本文从测试分层架构、框架 API 详解到实际测试编写,系统介绍鸿蒙应用的测试实践。

测试分层架构

在鸿蒙应用中,测试按覆盖范围从底向上分为三个层次,每一层都有明确的职责划分:

不依赖真机

需要真机/模拟器

需要真机/模拟器

测试分层架构

Unit 测试 / LocalUnit

Integration 测试 / ohosTest

UI 测试 / UiTest

纯 TypeScript/ArkTS 逻辑

工具函数测试

ViewModel 计算逻辑

数据模型验证

Ability 生命周期

系统 API 调用

模块间交互

页面元素定位

用户操作模拟

端到端流程验证

核心知识点

1. hypium 测试框架

hypium 是 HarmonyOS 的官方测试框架,采用 BDD(行为驱动开发)风格,提供简洁的 describe/it/expect API:

import { describe, it, expect } from '@ohos/hypium';

describe('Calculator Tests', () => {
  it('should add two numbers correctly', () => {
    const result = add(1, 2);
    expect(result).assertEqual(3);
  });

  it('should handle edge cases', () => {
    expect(0.1 + 0.2).assertClose(0.3, 0.001);
    expect(() => divide(1, 0)).assertThrowError('Division by zero');
  });
});

hypium 核心 API 一览:

API 功能 示例
describe(title, fn) 定义测试套件 describe('HomeVM', () => {})
it(title, fn) 定义测试用例 it('should init data', () => {})
expect(actual) 创建断言对象 expect(result)
.assertEqual(expected) 断言相等 expect(a).assertEqual(3)
.assertTrue() 断言为真 expect(flag).assertTrue()
.assertContain(item) 断言包含 expect(list).assertContain('a')
.assertClose(expected, delta) 断言近似相等 expect(0.1+0.2).assertClose(0.3, 0.001)
.assertThrowError(msg?) 断言抛出异常 `expect(fn).assertThrowError()’
beforeAll(fn) 套件前执行一次 beforeAll(() => initMock())
afterAll(fn) 套件后执行一次 afterAll(() => cleanup())
beforeEach(fn) 每条用例前执行 beforeEach(() => resetState())
afterEach(fn) 每条用例后执行 afterEach(() => clearData())

2. LocalUnit 本地测试

LocalUnit 测试不依赖真机,在本地运行,适合测试纯逻辑代码。MoneyTrack 项目中的测试目录结构如下:

src/test/
├── LocalUnit.test.ets     # 本地测试入口
└── unittest/
    ├── home/              # Home 模块测试
    └── common/            # 公共工具测试

测试工具函数示例:

// src/test/unittest/common/DateUtils.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { formatDate, parseAmount } from '../../../../commons/commonlib/src/main/ets/utils/DateUtils';

describe('DateUtils', () => {
  it('formatDate should return YYYY-MM-DD format', () => {
    const date = new Date(2024, 0, 15); // 2024-01-15
    expect(formatDate(date)).assertEqual('2024-01-15');
  });

  it('formatDate should pad single digit month/day', () => {
    const date = new Date(2024, 8, 5); // 2024-09-05
    expect(formatDate(date)).assertEqual('2024-09-05');
  });

  it('parseAmount should handle integer string', () => {
    expect(parseAmount('100')).assertEqual(100);
  });

  it('parseAmount should handle decimal string', () => {
    expect(parseAmount('99.99')).assertClose(99.99, 0.001);
  });

  it('parseAmount should return 0 for invalid input', () => {
    expect(parseAmount('')).assertEqual(0);
    expect(parseAmount('abc')).assertEqual(0);
  });
});

测试 ViewModel 逻辑示例:

// src/test/unittest/home/HomeVM.test.ets
import { describe, it, expect, beforeEach } from '@ohos/hypium';
import { HomeVM } from '../../../../features/home/src/main/ets/viewmodel/HomeVM';

describe('HomeVM', () => {
  let vm: HomeVM;

  beforeEach(() => {
    vm = new HomeVM();
  });

  it('initial state should have empty bill list', () => {
    expect(vm.billList).assertEqual([]);
    expect(vm.totalIncome).assertEqual(0);
    expect(vm.totalExpense).assertEqual(0);
  });

  it('should calculate totals correctly', () => {
    vm.addBill({ amount: 100, type: 'expense' });
    vm.addBill({ amount: 200, type: 'income' });
    vm.addBill({ amount: 50, type: 'expense' });
    expect(vm.totalExpense).assertEqual(150);
    expect(vm.totalIncome).assertEqual(200);
  });

  it('should filter bills by date range', () => {
    vm.addBill({ amount: 100, date: '2024-01-15', type: 'expense' });
    vm.addBill({ amount: 200, date: '2024-02-01', type: 'income' });
    const filtered = vm.getBillsByRange('2024-01-01', '2024-01-31');
    expect(filtered.length).assertEqual(1);
    expect(filtered[0].amount).assertEqual(100);
  });
});

3. ohosTest 集成测试

ohosTest 需要在真机或模拟器上运行,可以访问系统 API 和 UI 组件。每个模块的 src/ohosTest/ 目录下存放集成测试代码。

测试 Ability 生命周期:

// features/home/src/ohosTest/ets/test/Ability.test.ets
import { describe, it, expect } from '@ohos/hypium';
import { UIAbilityContext } from '@kit.AbilityKit';

describe('EntryAbility Lifecycle Test', () => {
  let abilityContext: UIAbilityContext;

  // 模拟 Ability 启动
  beforeAll(async () => {
    abilityContext = await createTestAbility('EntryAbility');
  });

  it('should create ability successfully', () => {
    expect(abilityContext).assertTrue();
    expect(abilityContext.abilityInfo.name).assertEqual('EntryAbility');
  });

  it('should load main page on start', async () => {
    const pageUrl = abilityContext.getLastPageUrl();
    expect(pageUrl).assertContain('pages/Index');
  });

  it('should handle onForeground correctly', async () => {
    await abilityContext.onForeground();
    expect(abilityContext.isForeground).assertTrue();
  });

  it('should handle onBackground correctly', async () => {
    await abilityContext.onBackground();
    expect(abilityContext.isForeground).assertFalse();
  });

  it('should restore state after onNewWant', async () => {
    const want = { parameters: { target: 'settings' } };
    await abilityContext.onNewWant(want);
    const pageUrl = abilityContext.getLastPageUrl();
    expect(pageUrl).assertContain('pages/Settings');
  });

  afterAll(() => {
    destroyTestAbility(abilityContext);
  });
});

测试用例组织(List.test.ets):

// features/home/src/ohosTest/ets/test/List.test.ets
import { describe } from '@ohos/hypium';
import { abilityTest } from './Ability.test';
import { homeVMTest } from './HomeVM.test';

export function entryTest(): void {
  describe('EntryAbility Suite', abilityTest);
  describe('HomeVM Suite', homeVMTest);
}

4. LocalUnit 与 ohosTest 对比

对比维度 LocalUnit 本地测试 ohosTest 集成测试
运行环境 本地 IDE,不依赖真机 真机或模拟器
测试目录 src/test/ src/ohosTest/
系统 API 访问 不可访问 可完整访问
执行速度 极快(毫秒级) 较慢(秒级)
适用场景 工具函数、数据模型、ViewModel 逻辑 Ability 生命周期、UI 交互、系统服务
调试方式 直接运行断点调试 需连接设备调试
CI/CD 集成 可在流水线中直接运行 需要设备池或云真机
代码覆盖率 支持覆盖率收集 支持覆盖率收集

5. 测试覆盖率

hypium 测试框架支持收集测试覆盖率数据,帮助团队衡量测试的充分性。覆盖率指标包括:

  • 行覆盖率(Line Coverage):衡量代码中哪些行被执行过
  • 分支覆盖率(Branch Coverage):衡量条件判断的真假分支是否都被覆盖
  • 函数覆盖率(Function Coverage):衡量每个函数是否被调用

build-profile.json5 中开启覆盖率收集:

{
  "buildOption": {
    "coverage": {
      "enabled": true,
      "include": ["**/main/ets/**/*.ets"],
      "exclude": ["**/node_modules/**"]
    }
  }
}

MoneyTrack 项目要求核心业务模块的代码行覆盖率不低于 80%,工具函数模块不低于 90%。

最佳实践

  1. 测试金字塔原则:LocalUnit 测试应占 70% 以上,ohosTest 占 20%,UI 测试占 10%。本地测试速度快、维护成本低,应优先覆盖。

  2. 测试隔离:每条测试用例应相互独立,使用 beforeEach 重置状态,避免测试间数据污染。

  3. 依赖注入:在 ViewModel 和数据层中使用依赖注入,方便在测试时替换为 Mock 实现,避免对外部系统的依赖。

  4. 边界值覆盖:对工具函数测试时,除了正常输入外,一定要覆盖空值、负值、大数等边界情况。

  5. 测试命名规范:测试用例命名应清晰描述被测行为和预期结果,推荐格式:should [预期行为] when [条件],如 should return zero when amount is negative

  6. 持续集成:将 LocalUnit 测试集成到 CI/CD 流水线中,每次提交代码自动运行,确保新代码不会破坏已有功能。

推荐参考文档

  • HarmonyOS hypium 测试框架文档
  • LocalUnit 本地测试开发指南
  • ohosTest 集成测试配置说明
  • 测试覆盖率工具使用指南
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐