HarmonyOS掌上记账APP开发实践第79篇:鸿蒙测试框架实战 — 从单元测试到 UI 测试的全栈覆盖
鸿蒙测试框架实战 — 从单元测试到 UI 测试的全栈覆盖

文章简介
测试是保障代码质量的关键手段。HarmonyOS 提供了一套完整的测试框架 hypium,支持单元测试(LocalUnit)、集成测试(ohosTest)和 UI 测试等不同层次的测试。MoneyTrack 在各个模块中都配置了测试目录,通过 List.test.ets 和 Ability.test.ets 组织测试用例。本文从测试分层架构、框架 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%。
最佳实践
-
测试金字塔原则:LocalUnit 测试应占 70% 以上,ohosTest 占 20%,UI 测试占 10%。本地测试速度快、维护成本低,应优先覆盖。
-
测试隔离:每条测试用例应相互独立,使用
beforeEach重置状态,避免测试间数据污染。 -
依赖注入:在 ViewModel 和数据层中使用依赖注入,方便在测试时替换为 Mock 实现,避免对外部系统的依赖。
-
边界值覆盖:对工具函数测试时,除了正常输入外,一定要覆盖空值、负值、大数等边界情况。
-
测试命名规范:测试用例命名应清晰描述被测行为和预期结果,推荐格式:
should [预期行为] when [条件],如should return zero when amount is negative。 -
持续集成:将 LocalUnit 测试集成到 CI/CD 流水线中,每次提交代码自动运行,确保新代码不会破坏已有功能。
推荐参考文档
- HarmonyOS hypium 测试框架文档
- LocalUnit 本地测试开发指南
- ohosTest 集成测试配置说明
- 测试覆盖率工具使用指南
更多推荐




所有评论(0)