摘要

HUnit 是 OpenHarmony 官方提供的单元测试框架,用于对工具类、业务逻辑、数据模型进行自动化代码测试,校验函数入参、返回值、异常分支是否符合预期,是课程设计、毕设加分项与企业项目质量管控必备模块。API Version23 重构测试用例生命周期、异步测试断言、沙盒上下文隔离、测试日志输出逻辑,修复低版本异步测试断言失效、工具类上下文缺失、多用例污染数据、测试执行崩溃等兼容缺陷。旧项目升级 API23 后常出现:异步工具函数测试无结果、RDB/Preferences 测试初始化失败、多个测试用例数据互相干扰、测试日志空白等问题。本文基于 DevEco Studio API23+,完整讲解测试工程创建、同步 / 异步用例编写、工具类测试、数据库模拟测试、测试报告查看全流程,封装通用测试基类,配套文件工具、通知工具、数据库工具实战测试用例,输出测试隔离、上下文初始化、异步断言规范,汇总版本升级故障解决方案,为鸿蒙自动化测试开发提供标准化模板。

关键词

OpenHarmony;ArkTS;API Version23;HUnit;单元测试;自动化测试;异步测试;工具类测试

一、引言

1.1 单元测试开发背景

项目中大量工具函数(文件读写、网络请求、数据库 CRUD、权限校验)存在大量分支逻辑,手动逐个场景验证效率极低,且修改代码后无法快速验证是否引入 BUG。HUnit 单元测试可批量自动执行校验,通过断言判断返回值、状态、异常是否符合设计预期。 测试分层:

  1. 单元测试:仅测试独立工具类 / 纯函数(无 UI、不依赖页面),本文核心内容;
  2. UI 测试:测试页面点击、弹窗、列表等交互(依赖 XTest)。

OpenHarmony API Version23 HUnit 核心升级变更:

  1. 标准化测试套件生命周期:beforeAll → beforeEach → 测试用例 → afterEach → afterAll;
  2. 完善异步测试 async/await 断言支持,修复 Promise 场景捕获失效;
  3. 提供独立测试沙盒上下文,避免测试污染业务真实数据库 / 存储;
  4. 新增精准断言 API:相等、布尔、空值、数组、异常捕获;
  5. 隔离测试进程与应用主进程,测试操作不会覆盖用户真实数据;
  6. 优化控制台日志分级输出,区分成功 / 失败 / 异常用例日志。

大量旧项目升级后数据库工具测试报错、异步接口断言无响应,根源是未在生命周期钩子初始化独立测试上下文,未使用异步专用测试模板,因此掌握 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 同步 / 异步测试模板区分

  1. 同步用例:test("名称", ()=>{})
  2. 异步用例:test("名称", async ()=>{ await 逻辑; 断言 })

2.4 API23 强制约束

  1. 数据库、Preferences 工具必须在 beforeEach 初始化独立测试上下文;
  2. 每个用例 afterEach 清空测试沙盒数据,防止用例之间数据污染;
  3. 异步操作必须加 await,否则断言会提前执行导致用例误判成功;
  4. 禁止测试用例直接操作 entry 真实业务沙盒,仅使用测试隔离环境;
  5. 耗时 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 执行测试操作步骤

  1. DevEco 左侧测试窗口打开 HUnit;
  2. 选择测试设备 / 模拟器;
  3. 选择单个测试套件 / 全部用例点击运行;
  4. 底部控制台查看每条用例成功 / 失败日志;
  5. 失败用例日志会打印预期值、实际值、错误代码行。

5.3 测试分层规范

  1. 纯逻辑工具(计算、字符串处理):同步快速测试,无需上下文;
  2. 存储类工具(fs、Preferences、RDB):异步测试,必须重置沙盒;
  3. 网络工具:可模拟固定返回数据,避免真实接口依赖;
  4. 页面 UI、XComponent、Web 组件不适合单元测试,改用 XTest UI 自动化。

5.4 数据隔离规范

  1. 每个用例 afterEach 清空数据库、文件、存储,用例互不干扰;
  2. 测试独立沙盒与业务 APP 存储隔离,不会删除用户真实笔记、配置;
  3. 测试使用专用 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,可直接集成至现有四层架构工程。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐