OpenHarmony 单元测试 HUnit 自动化测试(API Version23+)
摘要
HUnit 是 OpenHarmony 官方提供的单元测试框架,用于对工具类、业务逻辑、数据模型进行自动化代码测试,校验函数入参、返回值、异常分支是否符合预期,是课程设计、毕设加分项与企业项目质量管控必备模块。API Version23 重构测试用例生命周期、异步测试断言、沙盒上下文隔离、测试日志输出逻辑,修复低版本异步测试断言失效、工具类上下文缺失、多用例污染数据、测试执行崩溃等兼容缺陷。旧项目升级 API23 后常出现:异步工具函数测试无结果、RDB/Preferences 测试初始化失败、多个测试用例数据互相干扰、测试日志空白等问题。本文基于 DevEco Studio API23+,完整讲解测试工程创建、同步 / 异步用例编写、工具类测试、数据库模拟测试、测试报告查看全流程,封装通用测试基类,配套文件工具、通知工具、数据库工具实战测试用例,输出测试隔离、上下文初始化、异步断言规范,汇总版本升级故障解决方案,为鸿蒙自动化测试开发提供标准化模板。
关键词
OpenHarmony;ArkTS;API Version23;HUnit;单元测试;自动化测试;异步测试;工具类测试
一、引言
1.1 单元测试开发背景
项目中大量工具函数(文件读写、网络请求、数据库 CRUD、权限校验)存在大量分支逻辑,手动逐个场景验证效率极低,且修改代码后无法快速验证是否引入 BUG。HUnit 单元测试可批量自动执行校验,通过断言判断返回值、状态、异常是否符合设计预期。 测试分层:
- 单元测试:仅测试独立工具类 / 纯函数(无 UI、不依赖页面),本文核心内容;
- UI 测试:测试页面点击、弹窗、列表等交互(依赖 XTest)。
OpenHarmony API Version23 HUnit 核心升级变更:
- 标准化测试套件生命周期:beforeAll → beforeEach → 测试用例 → afterEach → afterAll;
- 完善异步测试 async/await 断言支持,修复 Promise 场景捕获失效;
- 提供独立测试沙盒上下文,避免测试污染业务真实数据库 / 存储;
- 新增精准断言 API:相等、布尔、空值、数组、异常捕获;
- 隔离测试进程与应用主进程,测试操作不会覆盖用户真实数据;
- 优化控制台日志分级输出,区分成功 / 失败 / 异常用例日志。
大量旧项目升级后数据库工具测试报错、异步接口断言无响应,根源是未在生命周期钩子初始化独立测试上下文,未使用异步专用测试模板,因此掌握 HUnit 单元测试是规范工程必备能力。
1.2 开发环境与测试场景
开发工具:DevEco Studio 5.0+ 适配系统:OpenHarmony API23、HarmonyOS NEXT 依赖模块:@ohos.test.HUnit 工程结构:独立 hittest 测试模块,关联 entry 业务模块 测试场景:文件工具读写校验、Preferences 存取、RDB 增删改查、工具类入参异常拦截、异步网络请求模拟校验
二、API23+ HUnit 核心生命周期与断言 API
2.1 测试套件完整生命周期执行顺序
plaintext
beforeAll() // 整个测试套件执行前运行1次(全局初始化) ├─ beforeEach() // 单个用例执行前(重置数据、初始化上下文) │ ├─ test() // 单条测试用例 ├─ afterEach() // 单个用例执行后(清理测试数据) afterAll() // 全部用例执行完毕(销毁资源)2.2 核心断言 Assert 方法
ets
import Assert from '@ohos.test.HUnit.Assert' Assert.equal(actual, expect, "错误提示文案") // 判断相等 Assert.true(condition, "条件应为true") Assert.false(condition) Assert.notNull(value) Assert.isNull(value) Assert.throws(func, "预期抛出异常")2.3 同步 / 异步测试模板区分
- 同步用例:
test("名称", ()=>{}) - 异步用例:
test("名称", async ()=>{ await 逻辑; 断言 })
2.4 API23 强制约束
- 数据库、Preferences 工具必须在 beforeEach 初始化独立测试上下文;
- 每个用例 afterEach 清空测试沙盒数据,防止用例之间数据污染;
- 异步操作必须加 await,否则断言会提前执行导致用例误判成功;
- 禁止测试用例直接操作 entry 真实业务沙盒,仅使用测试隔离环境;
- 耗时 IO 操作不要放入 beforeAll,统一放到 beforeEach 单用例重置。
三、基础最简单元测试示例
3.1 同步纯函数测试(字符串工具)
ets
import { describe, test, beforeAll, beforeEach, afterEach, afterAll } from '@ohos.test.HUnit' import Assert from '@ohos.test.HUnit.Assert' // 待测试纯工具函数 function add(a:number,b:number):number { return a + b } describe("数学计算工具测试套件", ()=>{ test("两数相加正常返回结果", ()=>{ const res = add(2,3) Assert.equal(res,5,"2+3预期等于5") }) })3.2 异步文件读写测试基础模板
ets
describe("文件工具异步测试", ()=>{ let ctx: common.UIAbilityContext beforeEach(async ()=>{ // 获取测试独立上下文 ctx = getContext(this) FileUtil.setContext(ctx) }) test("写入并读取文本文件", async ()=>{ const path = ctx.filesDir + "/test_log.txt" const text = "单元测试文本" FileUtil.writeText(path, text) const readStr = FileUtil.readText(path) Assert.equal(readStr, text,"写入读取内容不一致") }) })四、三大完整实战测试用例(适配前文全部工具类)
4.1 实战一:通用测试基类封装(hittest/src/main/ets/BaseTest.ets)
ets
import { beforeAll, beforeEach, afterEach, afterAll } from '@ohos.test.HUnit' import common from '@ohos.app.ability.common' import FileUtil from '../../../entry/src/main/ets/utils/file_util' import PrefUtil from '../../../entry/src/main/ets/utils/preference_utils' import RdbUtil from '../../../entry/src/main/ets/utils/rdb_util' export default class BaseTest { protected ctx: common.UIAbilityContext | null = null // 套件全局初始化 initSuite() { beforeAll(()=>{ console.info("===== 测试套件全局初始化 =====") }) } // 每个用例前置:绑定上下文、初始化全部存储工具 initCase() { beforeEach(async ()=>{ this.ctx = getContext(this) FileUtil.setContext(this.ctx) PrefUtil.setContext(this.ctx) await PrefUtil.init(this.ctx) await RdbUtil.init(this.ctx) }) } // 每个用例后置:清空测试数据、释放数据库 clearCase() { afterEach(async ()=>{ if(this.ctx) { // 清空测试Preferences await PrefUtil.clearAll() // 关闭数据库释放锁 await RdbUtil.closeDB() // 清空测试文件目录 const dir = this.ctx.filesDir FileUtil.clearDir(dir) } }) } // 套件全部执行完毕销毁 destroySuite() { afterAll(()=>{ console.info("===== 全部测试用例执行完毕 =====") }) } }4.2 实战二:Preferences 持久化工具完整测试套件
文件路径:hittest/src/main/ets/PrefTest.ets
ets
import { describe, test } from '@ohos.test.HUnit' import Assert from '@ohos.test.HUnit.Assert' import PrefUtil from '../../../entry/src/main/ets/utils/preference_utils' import BaseTest from './BaseTest' // 初始化测试生命周期 const base = new BaseTest() base.initSuite() base.initCase() base.clearCase() base.destroySuite() describe("Preferences本地存储工具测试", ()=>{ test("写入、读取布尔值校验", async ()=>{ await PrefUtil.putBool("test_bool", true) const val = await PrefUtil.getBool("test_bool") Assert.true(val,"布尔值读取应为true") }) test("写入、读取字符串校验", async ()=>{ const testStr = "unit_test_string" await PrefUtil.putString("test_str", testStr) const res = await PrefUtil.getString("test_str") Assert.equal(res, testStr,"字符串读写不匹配") }) test("删除key后读取返回默认值", async ()=>{ await PrefUtil.putNumber("test_num", 999) await PrefUtil.deleteKey("test_num") const num = await PrefUtil.getNumber("test_num", 0) Assert.equal(num, 0,"删除key后应返回默认0") }) })4.3 实战三:RDB 数据库笔记 CRUD 测试套件
文件路径:hittest/src/main/ets/RdbNoteTest.ets
ets
import { describe, test } from '@ohos.test.HUnit' import Assert from '@ohos.test.HUnit.Assert' import RdbUtil from '../../../entry/src/main/ets/utils/rdb_util' import BaseTest from './BaseTest' const base = new BaseTest() base.initSuite() base.initCase() base.clearCase() base.destroySuite() describe("RDB笔记数据库CRUD单元测试", ()=>{ test("新增笔记,查询条数为1", async ()=>{ // 新增一条笔记 await RdbUtil.addNote("单元测试标题","单元测试内容") const list = await RdbUtil.queryNoteList(0,10) Assert.equal(list.length, 1,"新增后列表应仅有1条数据") }) test("删除笔记后列表为空", async ()=>{ const id = await RdbUtil.addNote("待删除笔记","内容") await RdbUtil.deleteNote(id) const list = await RdbUtil.queryNoteList(0,10) Assert.equal(list.length, 0,"删除后数据应清空") }) test("修改笔记标题校验", async ()=>{ const id = await RdbUtil.addNote("旧标题","内容") await RdbUtil.updateNote(id, "新标题","内容") const list = await RdbUtil.queryNoteList(0,10) Assert.equal(list[0].title, "新标题","标题修改未生效") }) })五、API23+ 单元测试工程配置与执行规范
5.1 hittest 模块 module.json5 基础配置
json
{ "module": { "name": "hittest", "type": "har", "description": "单元测试模块", "deviceTypes": ["phone"], "deliveryWithInstall": false, "dependencies": [ { "name": "entry", "version": "1.0.0", "scope": "shared" } ] } }5.2 执行测试操作步骤
- DevEco 左侧测试窗口打开 HUnit;
- 选择测试设备 / 模拟器;
- 选择单个测试套件 / 全部用例点击运行;
- 底部控制台查看每条用例成功 / 失败日志;
- 失败用例日志会打印预期值、实际值、错误代码行。
5.3 测试分层规范
- 纯逻辑工具(计算、字符串处理):同步快速测试,无需上下文;
- 存储类工具(fs、Preferences、RDB):异步测试,必须重置沙盒;
- 网络工具:可模拟固定返回数据,避免真实接口依赖;
- 页面 UI、XComponent、Web 组件不适合单元测试,改用 XTest UI 自动化。
5.4 数据隔离规范
- 每个用例 afterEach 清空数据库、文件、存储,用例互不干扰;
- 测试独立沙盒与业务 APP 存储隔离,不会删除用户真实笔记、配置;
- 测试使用专用 test_前缀 key / 表名,区分业务数据。
六、API23 升级高频兼容问题与解决方案
问题 1:RDB/Preferences 测试提示上下文为空 解决:在 beforeEach 生命周期钩子中获取测试专用上下文并传入工具初始化。
问题 2:异步读写文件断言直接判定成功,实际逻辑未执行完 解决:异步测试函数头部添加 async,IO 操作前加 await 等待执行完毕再断言。
问题 3:第一条用例正常,第二条用例读取到上一条残留数据 解决:afterEach 清空数据库、删除测试文件、清空 Preferences。
问题 4:运行测试后打开业务 APP,本地配置全部清空 解决:使用独立 hittest 测试沙盒,不要共用 entry 业务上下文。
问题 5:升级 API23 后旧版 runTest 执行函数编译报错 解决:全部替换为标准 describe+test 生命周期写法,废弃旧版单文件测试入口。
问题 6:测试控制台无日志输出,无法定位失败原因 解决:使用 Assert 第三个参数填写清晰错误描述,开启 DevEco 测试日志打印开关。
七、总结
HUnit 单元测试是 OpenHarmony 标准化自动化验证工具,API Version23 完善测试生命周期、异步断言、独立隔离沙盒三大核心能力,解决旧版异步校验失效、测试数据互相污染、上下文缺失崩溃等问题。项目封装统一测试基类,自动完成初始化、数据清理、资源销毁,配套 Preferences、RDB 数据库两大高频工具完整测试套件,覆盖增删改查、读写、删除全分支校验。 单元测试可大幅降低后期改代码产生 BUG 的风险,课程设计、毕设增加测试模块能显著提升项目完整度与答辩分数,全部代码兼容 API23 与 HarmonyOS NEXT,可直接集成至现有四层架构工程。
