鸿蒙 ohpm 包管理与 Hypium 测试框架

本文讲清鸿蒙工程的依赖与测试体系:ohpm 仓库与命令行用法、oh-package.json5 字段与版本约束语法、HAR 与 HSP 两种包形态的差异与选型、私仓与 ohpmrc 配置、Hypium 单元测试用例与断言写法、UI 测试、覆盖率统计与 DevEco 运行配置。

引言

鸿蒙工程的依赖与测试体系由两块组成:ohpm 负责「代码怎么被组织与复用」,Hypium 负责「代码怎么被验证」。两者看似无关,实际上紧密耦合——包形态的选择直接决定了测试能不能跑、覆盖率能不能统计、依赖方能不能拿到你的测试工具类。

工程上的难点在包形态与依赖边界。HAR 是静态包,代码会被编进使用方的产物;HSP 是动态包,运行时共享一份实例。选错了要么包体积暴涨,要么出现「两个模块各持有一个单例」的诡异 bug。而依赖的版本约束写错,则会在多模块协作时出现「本地能编译、CI 上失败」。

本文按「包管理、包形态、测试、覆盖率」四条线展开。工程结构与模块划分见 HarmonyOS NEXT 全景与开发环境搭建 ,本文不重复工程创建部分。

目录

  1. 包管理在鸿蒙工程中的位置
  2. ohpm 命令行基础
  3. oh-package.json5 字段详解
  4. 版本约束语法
  5. 依赖类型与作用域
  6. HAR 静态共享包
  7. HSP 动态共享包
  8. HAR 与 HSP 选型
  9. 私仓与 ohpmrc 配置
  10. Hypium 单元测试
  11. UI 测试与依赖打桩
  12. 覆盖率与测试运行配置
  13. 权衡取舍
  14. 常见坑清单
  15. 小结

1. 包管理在鸿蒙工程中的位置

鸿蒙工程是「一个工程多个模块」的结构,模块之间通过包依赖组织。

层级配置文件职责
工程级根目录 oh-package.json5声明全局依赖与共享的版本约束
模块级各模块 oh-package.json5声明该模块自己的依赖
锁定文件oh-package-lock.json5记录实际解析到的版本与校验值
仓库配置.ohpmrc指定仓库地址、认证信息、代理

一条硬规则:oh-package-lock.json5 必须提交到版本库。它保证团队与 CI 解析出完全相同的依赖树,不提交的话,别人拉下代码后 ohpm install 会拿到最新匹配版本,出现「我这能跑你那不能跑」的经典问题。

模块划分上,推荐按「entry 壳工程 + feature 业务模块 + common 公共模块」三层组织。entry 只放入口 Ability 与路由表,业务逻辑全部下沉到 feature 模块,跨模块复用的工具与类型放 common。这样做的收益在测试环节体现得最明显:feature 模块可以独立跑单元测试,不需要拉起整个应用。

2. ohpm 命令行基础

ohpm 是鸿蒙的包管理器,随 DevEco Studio 一起安装,也可以在 CI 上单独部署。

ohpm -v                              # 查看版本
ohpm install                         # 按 oh-package.json5 安装全部依赖
ohpm install @ohos/lottie            # 安装指定三方库
ohpm install ./libs/mylib.har        # 安装本地 HAR 包
ohpm update                          # 按版本约束升级依赖
ohpm uninstall @ohos/lottie          # 卸载
ohpm list                            # 列出当前依赖树
ohpm publish ./mylib.har             # 发布到私仓

ohpm install ./libs/mylib.har 这种本地安装方式在调试自研库时非常实用:改完库代码后重新打包、重新安装,比走私仓发布快得多。安装后依赖会记录在 oh-package.json5 里,路径形式依赖不会进 oh-package-lock.json5,因此本地路径依赖不能进发布流程。

CI 环境上推荐用 ohpm install --all 一次装齐所有模块的依赖,而不是逐个模块 cd 进去装。同时要设置 OHPM_HOME 环境变量指定缓存目录,否则每次构建都会重新下载。

3. oh-package.json5 字段详解

oh-package.json5 是依赖声明的核心文件,字段不多但每个都有明确语义。

{
  "name": "feature_reader",
  "version": "1.0.0",
  "description": "阅读器业务模块",
  "main": "Index.ets",
  "author": "Leeting Yan",
  "license": "Apache-2.0",
  "dependencies": {
    "@ohos/lottie": "^2.0.0",
    "common_utils": "file:../common"
  },
  "devDependencies": {
    "@ohos/hypium": "1.0.19",
    "@ohos/hamock": "1.0.0"
  },
  "dynamicDependencies": {
    "feature_live": "^1.0.0"
  }
}
字段作用注意点
name包名,模块间引用靠它改名会让所有引用方编译失败
version包版本发布 HAR 时必须递增
main对外导出入口文件不写则默认 Index.ets,路径错误会让 import 拿到空对象
dependencies运行时依赖会被打进产物
devDependencies开发期依赖不参与打包,测试框架放这里
dynamicDependencies动态依赖仅 HSP 支持,运行时按需加载

main 字段是最容易出问题的一个。它的路径是相对包根目录的,写 Index.ets 与 ./Index.ets 都可以,但写 src/Index.ets 就必须保证文件真的在那个位置。配置错误的表现是 import { X } from 'feature_reader' 编译通过但运行时 X 为 undefined,而且 IDE 通常不给提示。

4. 版本约束语法

ohpm 的版本约束沿用语义化版本规范,三种写法的语义差别很大。

写法含义允许升级到建议场景
1.0.19精确版本不升级测试框架、工具链
^1.0.19兼容版本1.x 的最高版多数三方库
~1.0.19补丁版本1.0.x 的最高版对次版本变更敏感时
1.x主版本内任意1.x 的最高版语义化版本严格遵循时
*任意版本最新仅原型验证,不要进生产

选择原则是「库越底层,约束越紧」。测试框架、构建工具这类对版本敏感的依赖用精确版本,避免 CI 上因为自动升级而出现难以复现的失败;业务三方库可以用 ^,减少手动升级的频率。

一个具体的坑:^1.0.19 在库作者没有严格遵守语义化版本时会带来破坏性升级。判断依据是看这个库的主版本号是否已经稳定(大于等于 1)。主版本为 0 的库用 ^ 风险很高,因为 0.x 系列允许任意破坏性变更,此时应该用精确版本或 ~。

5. 依赖类型与作用域

三种依赖类型决定了「代码是否进入产物」与「何时被加载」。

{
  "dependencies": {
    "@ohos/lottie": "^2.0.0"
  },
  "devDependencies": {
    "@ohos/hypium": "1.0.19",
    "@ohos/hamock": "1.0.0"
  },
  "dynamicDependencies": {
    "feature_live": "^1.0.0"
  }
}
类型打包行为加载时机典型内容
dependencies进入产物应用启动即可用业务三方库、公共模块
devDependencies不进产物仅构建与测试期Hypium、Hamock、lint 工具
dynamicDependencies不进主包运行时按需加载低频功能模块、大体积模块

把测试框架写进 dependencies 是高频错误:它会让 Hypium 被打进发布包,既增加体积又暴露测试代码。正确的做法是 @ohos/hypium 与 @ohos/hamock 一律放 devDependencies。

dynamicDependencies 只对 HSP 生效,它的收益是把低频模块从主包里摘出去,代价是首次加载有延迟。判断标准是「这个模块在首屏会不会用到」:会用到就不要动态化,用不到才考虑。

6. HAR 静态共享包

HAR(Harmony Archive)是静态共享包,本质上是一份可被编译进使用方的源码与资源集合。

// 库模块的 build-profile.json5
{
  "apiType": "stageMode",
  "buildOption": {},
  "targets": [
    {
      "name": "default",
      "applyToProducts": ["default"]
    }
  ]
}

HAR 的特点是编译期内联:使用方在编译时把 HAR 的代码合并进自己的产物,因此运行时只有一份代码副本在各自模块里。这带来两个直接后果:其一,HAR 里的全局单例在每个使用方那里都是独立的,跨模块共享状态会失效;其二,HAR 的代码会被重复打包,多个模块依赖同一个 HAR 会让包体积成倍增长。

HAR 里可以放 ArkTS 代码、资源文件、Native 库(.so),也可以放测试代码。HAR 的对外 API 必须提供 consumer-rules.txt,否则使用方开启混淆后调用你的接口会因为类名被改写而失败。

7. HSP 动态共享包

HSP(Harmony Shared Package)是动态共享包,运行时在多个模块之间共享同一份代码实例。

// HSP 模块的 module.json5 关键字段
{
  "module": {
    "name": "shared_common",
    "type": "shared",
    "description": "$string:shared_desc"
  }
}

HSP 的核心价值是单例共享:状态管理、网络客户端、数据库连接这类需要全局唯一的对象放在 HSP 里,所有模块拿到的是同一个实例。这与 HAR 的行为完全不同,也是选择 HSP 最主要的理由。

代价也很明确:HSP 必须随应用一起安装,不能单独更新;它的加载有初始化开销,首次调用会触发模块加载;HSP 之间不能循环依赖,依赖关系必须是有向无环图。HSP 的版本必须与应用版本同步发布,因为它不能独立升级。

// 使用 HSP 中的单例
import { GlobalStore } from 'shared_common';

// 所有模块拿到的是同一个实例
const store = GlobalStore.getInstance();

8. HAR 与 HSP 选型

两种包形态的差异集中在「是否共享实例」与「体积成本」上。

维度HARHSP
打包方式编译期内联运行期加载
实例数量每个使用方一份全局一份
包体积多模块依赖时成倍增长只存一份
能否独立更新随宿主更新不能,随应用更新
能否含 Native 库可以可以
动态依赖支持不支持支持
适用内容工具函数、UI 组件、纯逻辑全局单例、大体积公共模块

选型判断只有两条:需要跨模块共享状态就用 HSP,其余一律用 HAR。UI 组件库、工具函数、类型定义这些无状态的代码用 HAR 更简单,不需要关心加载时序;而网络客户端、状态容器、数据库封装这类有状态对象必须用 HSP,否则每个模块一个实例会导致数据不一致。

一个常见的误判是把「体积大」当成选 HSP 的理由。体积优化的正确手段是 dynamicDependencies 加低频模块拆分,而不是把所有公共代码都塞进 HSP——HSP 自身的加载开销与依赖约束会带来新的复杂度。

9. 私仓与 ohpmrc 配置

三方库默认从 ohpm 官方仓库拉取,企业内网环境或自研库需要配置私仓。

registry=https://ohpm.openharmony.cn/ohpm/
@mycompany:registry=https://ohpm.mycompany.com/repo/
strict_ssl=true
cache=~/.ohpm/cache

@mycompany:registry 这种带作用域的前缀写法可以只把某个 scope 的包指向私仓,其余仍走官方仓库,这是混合使用的最佳实践。strict_ssl=true 建议保持开启,关闭会带来中间人风险。

认证信息不要写进 .ohpmrc 并提交到版本库,正确做法是通过环境变量注入:

export OHPM_AUTH_TOKEN="$(cat /run/secrets/ohpm_token)"
ohpm install --all

CI 上尤其要注意这一点。把 token 写进配置文件并提交,等同于把私仓的读写权限公开,而撤销 token 需要重新配置所有构建节点。

10. Hypium 单元测试

Hypium 是鸿蒙的测试框架,API 风格与主流的 xUnit 接近,用 describe 组织用例、it 定义单个用例、expect 做断言。

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

export default function readerTest() {
  describe('ReadingProgress', () => {
    let calculator: ProgressCalculator;

    beforeAll(() => {
      calculator = new ProgressCalculator();
    });

    beforeEach(() => {
      calculator.reset();
    });

    it('should_return_zero_for_empty_input', 0, () => {
      expect(calculator.percent('')).assertEqual(0);
    });

    it('should_clamp_percent_to_100', 0, () => {
      calculator.update(120, 100);
      expect(calculator.percent()).assertEqual(100);
    });

    it('should_handle_async_load', 0, async (done: Function) => {
      const result = await calculator.loadFromCache('book_1');
      expect(result).assertTrue();
      done();
    });
  });
}

it 的第二个参数是过滤级别(0 表示必跑),第三个参数是用例体。异步用例必须显式接收 done 回调并调用它,否则测试框架会认为用例未结束,表现为「测试一直挂起直到超时」。

Hypium 的断言方法比较丰富,选对断言能让失败信息更可读:

断言语义适用
assertEqual严格相等数值与字符串比较
assertTrue / assertFalse布尔判断条件校验
assertNull / assertUndefined空值判断可选值校验
assertThrowError断言抛异常异常路径覆盖
assertClose浮点近似相等浮点计算
assertInstanceOf类型判断多态场景

浮点比较不要用 assertEqual,用 assertClose 指定误差范围,否则会因为精度问题出现偶发失败。

11. UI 测试与依赖打桩

UI 测试通过 @ohos.UiTest 驱动真实界面,适合验证跨组件的交互流程。

import { describe, it, expect } from '@ohos/hypium';
import { Driver, ON } from '@ohos.UiTest';

export default function uiTest() {
  describe('ReaderFlow', () => {
    it('should_open_detail_on_click', 0, async () => {
      const driver = Driver.create();
      await driver.delayMs(1000);
      // 通过文本定位控件并点击
      await driver.assertComponentExist(ON.text('打开书籍'));
      const button = await driver.findComponent(ON.text('打开书籍'));
      await button.click();
      await driver.delayMs(500);
      await driver.assertComponentExist(ON.text('目录'));
    });
  });
}

UI 测试的三条纪律:控件定位优先用稳定标识(ON.id 优于 ON.text,因为文案会变);每次交互后加等待,UI 渲染不是同步的;用例之间要互相独立,不能依赖上一个用例留下的状态。

依赖打桩用 Hamock 完成,它的典型用途是把网络请求与数据库替换成可控的假实现:

import { MockKit, when } from '@ohos/hamock';

const mock = new MockKit();
const httpMock = mock.mockFunc(HttpUtil, 'get');
when(httpMock).apply('articles').afterReturn(Promise.resolve('[]'));

打桩的前提是被测代码的依赖可以被替换。这反过来要求业务代码做依赖注入——如果 Repository 内部直接 new HttpUtil(),就没法替换。可测试性是设计出来的,不是测出来的,这一点与 前端测试策略 里讲的结论一致:把依赖从内部构造改为外部传入,是提升可测性收益最大的重构。

12. 覆盖率与测试运行配置

测试的运行入口在模块的 build-profile.json5 中配置,DevEco Studio 会读取它生成测试任务。

{
  "targets": [
    {
      "name": "default",
      "applyToProducts": ["default"],
      "runtimeOS": "HarmonyOS"
    }
  ]
}

覆盖率统计在 DevEco 的测试面板里开启,它会按文件粒度给出行覆盖率与分支覆盖率。几条实践建议:

  • 不要追求 100% 覆盖率,把目标定在核心业务逻辑的行覆盖率达到 70% 以上即可,UI 胶水代码的覆盖率意义不大。
  • 重点覆盖分支而不是行数,一个 if 的两个分支都跑过才算真正验证了逻辑。
  • 把测试命令接进 CI,用 hvigorw test 触发,失败即阻断合并,否则测试会在两周内腐化。
hvigorw --mode module -p module=feature_reader@default test \
  --coverage --coverage-output ./reports

覆盖率报告要作为构建产物归档,而不是只在本地看。覆盖率是趋势指标而不是门禁指标,它的价值在于「这次改动让覆盖率掉了多少」,而不是「有没有达到 80%」。把它当门禁会催生大量无断言的凑数用例,反而降低测试质量。

权衡取舍

包管理与测试的取舍集中在「复用便利」与「构建复杂度」之间。

决策点方案 A方案 B建议
包形态HAR(静态内联)HSP(动态共享)无状态用 A,全局单例用 B
版本约束精确版本^ 兼容版本工具链用 A,业务库用 B
依赖安装私仓发布本地路径依赖开发期用 B,进主干前换 A
测试范围全量覆盖核心逻辑覆盖优先 B,覆盖率当趋势看
大体积模块常驻主包dynamicDependencies首屏不用才动态化

一个容易被低估的成本是构建时间。HAR 越多,编译期需要处理的代码就越多;HSP 越多,运行期的模块加载次数就越多。团队规模较小时,模块拆分的收益往往抵不过构建变慢的成本,建议在模块数量超过十个之后再认真做包形态的划分。

常见坑清单

  1. oh-package-lock.json5 未提交。 团队与 CI 解析出不同依赖树,出现「本地能跑 CI 挂」。
  2. 测试框架写进 dependencies。 Hypium 被打进发布包,体积增加且暴露测试代码。
  3. main 字段路径错误。 import 编译通过但运行时拿到 undefined,IDE 通常不提示。
  4. 本地路径依赖进了主干。 file:../common 在 CI 上路径不存在,构建直接失败。
  5. 主版本为 0 的库用 ^。 允许任意破坏性升级,一次 ohpm update 就可能编译失败。
  6. HAR 里的单例被多个模块依赖。 每个模块拿到独立实例,状态不共享且难以排查。
  7. HSP 之间循环依赖。 依赖关系必须是有向无环图,环会导致加载失败。
  8. HAR 未提供 consumer-rules.txt。 使用方开混淆后调用接口失败,仅发布包复现。
  9. 认证 token 写进 .ohpmrc 并提交。 等同于公开私仓读写权限。
  10. 异步用例未调用 done()。 测试挂起直到超时,误判为用例执行慢。
  11. 浮点比较用 assertEqual。 精度问题导致偶发失败,改用 assertClose。
  12. UI 测试用文案定位控件。 文案一改用例全挂,应优先用 ON.id。

小结

包管理与测试在鸿蒙工程里是一体的:包形态决定了依赖边界,依赖边界决定了可测性。记住三条选择原则:需要跨模块共享状态用 HSP、其余用 HAR;工具链依赖用精确版本、业务库用 ^;测试框架一律放 devDependencies。测试侧则记住三条纪律:异步用例必须调 done()、浮点比较用 assertClose、覆盖率当趋势指标而不是门禁指标。

最后一条工程建议:把「可测试性」写进代码评审的检查项。依赖是否可以从外部注入、纯逻辑是否与 UI 解耦、模块边界是否清晰,这三条决定了单元测试能不能写、写起来贵不贵。测试写不下去,通常不是测试框架的问题,而是设计的问题。想继续了解模块划分与工程结构,可以回看 HarmonyOS NEXT 全景与开发环境搭建 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「鸿蒙开发」更多文章

  1. ArkUI 动画体系与手势交互
  2. 鸿蒙应用安全:权限模型与 HUKS 密钥管理
  3. 鸿蒙分布式软总线与跨设备迁移