【寻迹校园 HarmonyOS NEXT 实战 48】不新增测试依赖:用 Node Loader 直接测试生产 ArkTS 纯规则与状态机

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

Node Loader 直测生产 ArkTS 原创封面图

上图是原创技术概念图,不是测试终端截图。左侧生产模块经过 Loader 和系统桩后进入四条测试通道,绿色结果只代表对应纯规则和内存回退测试通过。

一、为什么不直接再写一套 JavaScript 规则

筛选、状态机和脱敏逻辑都已经存在于生产 .ets 文件。如果测试重新用 JavaScript 实现一遍,相当于维护两套规则:测试通过只能证明副本正确,不能证明 App 实际调用的生产实现正确。

因此目标是让 Node 直接加载生产 ArkTS 源文件,只替换无法在桌面 Node 环境运行的系统 Kit。这样测试仍调用 ReportServiceClaimServiceHandoffServiceModerationService 和真实模型类型。

二、为什么本阶段不引入 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-coreshared_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.LOCALDEVICEsetData(),确认没有 getData(),并检查页面文案要求显式复制与粘贴。

这种测试只能证明源码约束仍存在,真实系统剪贴板行为必须用设备验证。文章会明确区分二者。

十五、测试架构全景

生产 ArkTS、Loader、系统桩与测试矩阵原创结构图

上图的上层是生产规则和 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 应用构建和安装踩坑复盘》。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐