【寻迹校园 HarmonyOS NEXT 实战 48】不新增测试依赖:用 Node Loader 直接测试生产 ArkTS 纯规则与状态机
【寻迹校园 HarmonyOS NEXT 实战 48】不新增测试依赖:用 Node Loader 直接测试生产 ArkTS 纯规则与状态机
本章导读:这是“寻迹校园 HarmonyOS NEXT 实战”系列第 48 篇。项目需要快速回归筛选规则、Repository 本地回退、认领/交接/治理状态机和小艺脱敏逻辑,但不希望为了几组纯规则测试引入 Jest、修改 lockfile 或复制一套 JavaScript 业务实现。本文介绍项目里的
scripts/ets-loader.mjs:利用 Node ESM Loader 解析.ets、处理模块别名、剥离 TypeScript 类型,并用最小系统桩直接执行生产 ArkTS 模块。

上图是原创技术概念图,不是测试终端截图。左侧生产模块经过 Loader 和系统桩后进入四条测试通道,绿色结果只代表对应纯规则和内存回退测试通过。
一、为什么不直接再写一套 JavaScript 规则
筛选、状态机和脱敏逻辑都已经存在于生产 .ets 文件。如果测试重新用 JavaScript 实现一遍,相当于维护两套规则:测试通过只能证明副本正确,不能证明 App 实际调用的生产实现正确。
因此目标是让 Node 直接加载生产 ArkTS 源文件,只替换无法在桌面 Node 环境运行的系统 Kit。这样测试仍调用 ReportService、ClaimService、HandoffService、ModerationService 和真实模型类型。
二、为什么本阶段不引入 Jest
Jest、转换器和 ArkTS 适配层当然可以提供更完整的测试体验,但也会带来依赖、配置、缓存和转换兼容成本。当前项目的纯规则测试只需要:
- 顺序运行若干测试脚本;
- 使用 Node 原生
assert; - 支持 ESM import;
- 读取并转换
.ets; - 在失败时返回非零退出码。
Node 自带能力已经够用,所以项目没有新增测试依赖,也没有修改 oh-package-lock.json5 或 npm lockfile。
三、Loader 的两个职责:resolve 与 load
自定义 ESM Loader 暴露 resolve() 和 load()。前者决定一个 import 指向哪个文件,后者决定如何把该文件转换成 Node 可执行的模块。
项目命令使用:
node --no-warnings --experimental-loader ./scripts/ets-loader.mjs ./scripts/local-state-machines.test.mjs
这里的 --no-warnings 只压制实验性 Loader 提示,不会吞掉断言失败或业务异常。
四、模块别名不能在测试里硬改
生产代码使用 @xunji/common-core 和 shared_business 等别名。Loader 将它们映射到项目真实入口:
export async function resolve(specifier, context, nextResolve) {
if (specifier === '@xunji/common-core') {
return { shortCircuit: true, url: new URL('../common-core/Index.ets', import.meta.url).href };
}
if (specifier === 'shared_business') {
return { shortCircuit: true, url: new URL('../shared-business/Index.ets', import.meta.url).href };
}
return nextResolve(specifier, context);
}
测试脚本因此不需要把生产 import 改成相对路径,也不会污染 App 构建配置。
五、相对 import 自动补 .ets
ArkTS 文件中的相对导入可能省略扩展名,而 Node 默认不会自动尝试 .ets。Loader 在父模块存在时构造候选路径,检查文件是否存在,再返回短路解析结果。
这项处理必须保守:只有相对路径且没有 .ets 扩展名时才尝试,找不到就交回 nextResolve(),避免误吞 Node 内置模块或第三方包解析。
六、使用 Node 自带类型剥离
.ets 文件不能直接作为普通 JavaScript 执行。项目读取 UTF-8 源码后,使用 Node 的 stripTypeScriptTypes() 以 transform 模式去除类型语法:
export async function load(url, context, nextLoad) {
if (!url.endsWith('.ets')) return nextLoad(url, context);
const source = await readFile(new URL(url), 'utf8');
return {
format: 'module',
shortCircuit: true,
source: stripTypeScriptTypes(source, { mode: 'transform', sourceUrl: url })
};
}
它适合当前纯 TypeScript 风格的模型和 Service,不等于完整 ArkTS 编译器。涉及 ArkUI 装饰器、资源引用或平台语法时,仍应交给 hvigor。
七、系统 Kit 只做最小桩
生产 Repository 可能 import AbilityKit、ArkData、CoreFileKit 和 hilog。Node 环境没有这些模块,所以 Loader 映射到 scripts/stubs/ 下的最小实现。
桩的原则是“只提供测试路径实际使用的表面”,而不是模拟整个 HarmonyOS:
- AbilityKit 提供必要的上下文类型占位;
- ArkData 允许代码进入无
UIAbilityContext的内存回退; - CoreFileKit 提供不会触发真实设备 I/O 的最小接口;
- hilog 接收日志调用但不伪造系统日志行为。
如果业务开始依赖更复杂的设备语义,应升级到设备集成测试,而不是无限扩张桩。
八、直接调用生产 Service
状态机测试导入真实 Repository 和 Service:
import { claimService } from '../shared-business/src/main/ets/services/ClaimService.ets';
import { handoffService } from '../shared-business/src/main/ets/services/HandoffService.ets';
import { moderationService } from '../shared-business/src/main/ets/services/ModerationService.ets';
测试先插入最小报告,再调用提交认领、审核、交接改期、双方完成和治理隐藏。断言关注状态迁移和副作用,而不是页面文案。
九、Repository 隔离要防止引用泄漏
内存回退如果直接返回内部对象引用,调用方修改一次对象就可能悄悄污染 Repository。测试专门读取记录、修改返回对象,再次读取并断言原值不变。
同样,认领私密证明只能通过 findWithProof() 读取;公开列表不应包含 proof 字段。这类断言既验证数据隔离,也验证隐私边界。
十、状态机测试覆盖哪些关键迁移
当前 local-state-machines.test.mjs 覆盖:
- 认领提交的私密证明与确认门禁;
- 重复认领保护;
- 接受、拒绝、取消后的 Report 状态联动;
- 交接只允许改期一次;
- 交接超时后 Claim 与 Report 恢复;
- 双方确认后才完成 Claim 和两条 Report;
- 举报去重、受理顺序、隐藏与驳回副作用;
- 自有记录禁止举报。
这些是纯业务规则,适合快速在 Node 中回归。
十一、固定数据与时间边界
测试使用固定日期字符串、显式时间戳和极短的 ID 间隔,避免依赖真实页面输入。对需要过期的交接记录,测试直接把 expiresAt 设置到当前时间之前,再调用生产 Service 触发访问时过期检查。
这能验证状态机的时间比较,但不证明系统后台定时任务存在。项目当前过期检查发生在业务访问路径,文章不会把它描述成后台调度。
十二、筛选规则单独做纯函数矩阵
report-filter-policy.test.mjs 验证六维筛选的 AND 语义、类别精确匹配、地点包含匹配、颜色只读取公开标题和描述、未来/非法日期排除,以及今天/三天/七天边界。
纯函数测试不需要 Repository 或系统桩,失败时能更快定位到筛选策略本身。它和完整状态机测试分开,可以减少故障定位范围。
十三、小艺适配器测试隐私和长度
xiaoyi-agent-adapter.test.mjs 使用包含模拟手机号、证件号和账号的测试数据,断言输出中不再出现原始标识,并检查:
- 候选最多 3 条;
- 文本最长 240 字;
- 候选 4、5 不进入摘要;
- 复制前再次脱敏;
- 空摘要禁止复制。
测试数据是人工构造样例,不是真实用户信息。
十四、源码合约测试显式复制边界
剪贴板依赖系统 Kit,不适合在 Node 中伪造一次“真实复制成功”。项目的 xiaoyi-copy-handoff.test.mjs 采用源码合约检查:确认存在 ShareOption.LOCALDEVICE 和 setData(),确认没有 getData(),并检查页面文案要求显式复制与粘贴。
这种测试只能证明源码约束仍存在,真实系统剪贴板行为必须用设备验证。文章会明确区分二者。
十五、测试架构全景

上图的上层是生产规则和 Service,中层是 Loader 与别名解析,右侧是最小系统桩,下层是隔离数据、时间控制、断言和结果。绿色并不代表系统 Kit 真机通过,只代表对应测试入口通过。
十六、统一 PowerShell 入口
scripts/test-local.ps1 顺序执行四组脚本,并在任一退出码非零时立即停止:
$tests = @(
'./scripts/report-filter-policy.test.mjs',
'./scripts/local-state-machines.test.mjs',
'./scripts/xiaoyi-agent-adapter.test.mjs',
'./scripts/xiaoyi-copy-handoff.test.mjs'
)
foreach ($test in $tests) {
node --no-warnings --experimental-loader ./scripts/ets-loader.mjs $test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
这个脚本适合本地预检,也可以被更上层流程调用,但项目当前没有因此修改 CI。
十七、2026-08-28 当天结果
本文准备时运行 scripts/test-local.ps1,四组入口全部通过:
| 测试入口 | 当天结果 |
|---|---|
| 筛选策略 | passed |
| Repository 与本地状态机 | passed |
| 小艺摘要脱敏、候选数与长度 | passed |
| 显式复制源码合约 | passed |
随后完整 APP 构建也返回 BUILD SUCCESSFUL。测试通过与构建通过是两份独立证据,不能互相替代。
十八、Node Loader 不能证明什么
这套测试不证明:
- RelationalStore 在真实设备上的建库、迁移和并发行为;
- Photo Picker、剪贴板、Agent Framework Kit 等系统 Kit 行为;
- ArkUI 页面布局、焦点、暗色模式和生命周期;
- HAP/HSP 安装、冷启动与进程存活;
- 签名、AGC 上传或商店审核。
它的价值是快速保护纯规则和无上下文内存回退,不是用 Node 冒充 HarmonyOS 设备。
十九、何时应该升级测试方案
出现以下情况时,应考虑增加 hvigor 测试、设备集成测试或更正式的测试框架:
- 生产代码大量使用 ArkUI 装饰器和资源;
- Repository 依赖真实 RelationalStore 迁移;
- 需要并发、事务或多进程验证;
- 需要 Mock 网络时钟、重试和取消;
- 团队需要覆盖率、筛选执行或 CI 报告;
- Node 的类型剥离不再支持项目语法。
升级应由测试需求驱动,而不是为了工具数量。
二十、小结
轻量测试的关键不是“少写配置”,而是让测试尽可能靠近生产实现。寻迹校园用自定义 Node Loader 解决 .ets、别名和最小系统依赖,让筛选、Repository、状态机和脱敏规则可以快速回归,同时保持依赖和 lockfile 不变。
最重要的边界也同样清晰:Node 测试证明纯规则,不证明设备 Kit;完整 APP 构建证明可打包,不证明运行。把证据拆开,测试才不会因为“全部绿色”而制造错误安全感。
下一篇:《【寻迹校园 HarmonyOS NEXT 实战 49】HAP、HSP 与 APP 的区别:多模块 HarmonyOS 应用构建和安装踩坑复盘》。
更多推荐

所有评论(0)