【万能转换器|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击

【万能转换器|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击

本轮证据边界: 当前源码共有 65 个 it() 声明,其中 UnitConverter 14 个、CalcEngine 42 个、History 8 个、Ability 1 个。本轮只进行了静态源码复核,没有执行测试套件、构建、安装或真机回归,因此"65 个源码用例"绝不等于"65 个测试已通过"。

启动、空数据、异常输入、重复点击和重启恢复属于基于现有缺口提出的设备回归矩阵;除文中明确引用的现有测试外,不把建议测试描述为当前工程已经实现。

工具类应用的回归测试有一个常见误区:公式算对了,就认为页面稳定。实际发布后更容易暴露的问题,往往发生在公式之外:服务还没初始化时页面已经可点、空列表进入编辑模式、非法输入留下上一次结果、连续点击"转换"写入多条历史、异步删除过程中再次操作、默认单位刚修改就返回、剪贴板写入失败却没有反馈。这些状态很难靠一条"1 米等于 100 厘米"覆盖。

"万能转换器"的真实源码已经具备一层不错的算法测试。entry/src/ohosTest/ets/test 中的 List.test.ets 聚合了 Ability.test.etsUnitConverter.test.etsCalcEngine.test.etsHistory.test.ets,按源码中的 it() 声明统计共有 65 个用例:14 个单位换算用例、42 个计算引擎用例、8 个历史模型用例和 1 个模板式 Ability 断言。它们覆盖温度非线性换算、NaN/Infinity 格式化、零利率、无效 BMI、90 度正切、错误进制、序列化容错等边界。

但"有 65 个测试文件中的用例"不等于"本次已经执行并全部通过",本文没有伪造测试运行结果。源码证据还显示,现有 ohosTest 主要保护纯函数和模型;Ability.test.ets 只是模板断言,没有真实启动、路由和页面操作;未发现针对 ConvertPageHistoryPageFavoriteToolsPageDefaultUnitsPage 的 UI 自动化脚本。尤其是重复点击、异步服务未就绪、删除中途失败和重启后的持久化恢复,仍需要设备级回归。

本文面向 HarmonyOS 5.0 及以上版本,基于上述测试源码和五个真实页面,建立"纯函数单测 + 服务状态测试 + ArkUI 页面回归 + 安装启动稳定性"的分层方案。

唯一标记:AGC18-HMOS-18-19-REGRESSION-STARTUP-EMPTY-INVALID-REPEAT

一、先把"已有覆盖"和"计划覆盖"分开

测试文章最忌讳把测试计划写成测试结果。当前源码能确认的事实如下:

层级 已有源码 可确认覆盖 仍缺少的证据
单位换算 UnitConverter.test.ets 14 个声明用例 本次执行报告
计算引擎 CalcEngine.test.ets 40 个声明用例 本次执行报告
历史模型 History.test.ets 8 个声明用例 Preferences 真机读写
Ability Ability.test.ets 1 个模板断言 真实安装与启动
ArkUI 页面 未发现对应 UI 脚本 路由、空态、点击、弹窗、返回

本文后续出现的 UI 场景、测试代码和门禁值,若没有在当前工程实现,都会明确标为"建议方案"。这能避免把未来工作误写成现状,也便于团队逐条落地。

二、现有63个用例保护了什么

List.test.ets 的聚合关系很直接:

ts 复制代码
export default function testsuite() {
  abilityTest();
  unitConverterTest();
  calcEngineTest();
  historyModelTest();
}

其中真正有业务价值的是三组纯逻辑测试:

  • 单位换算:长度、重量、温度、数据容量、时间、速度、批量换算与格式化。
  • 计算引擎:房贷、BMI、体脂、个税、利息、欧姆定律、合阻、几何、日期、汇率、进制和金额大写。
  • 历史模型:ID、序列化往返、空值、畸形 JSON、缺失字段补默认值和时间格式化。

这些用例执行快、定位准,适合作为每次提交的第一道门禁。它们解决"计算核是否被改坏",却不能回答页面是否能正常启动和交互。

三、现有用例中的边界设计值得保留

测试没有只写正常样例,而是主动覆盖异常值:

ts 复制代码
it('convertAll with NaN propagates NaN', 0, () => {
  const results = convertAll(group, 'm', NaN);
  for (const result of results) {
    expect(Number.isNaN(result.value)).assertTrue();
    expect(result.display).assertEqual('-');
  }
});

CalcEngine.test.ets 还检查:

text 复制代码
BMI 输入 0 → 无效等级
90° 正切 → NaN
错误进制字符串 → valid=false
无效长度估算 → 空数组
零利率房贷 → 本金按期数均分
并联电阻 → 忽略 0 和负值

这类用例建立了稳定的领域合同。页面回归不必再次证明所有数学公式,只要验证 UI 能把非法输入映射到这些既定结果,并且不保存错误历史。

四、启动回归的风险在异步初始化

EntryAbility.onCreate() 会启动三个异步服务:

ts 复制代码
HistoryService.init(this.context).catch(...);
SettingsService.init(this.context).catch(...);
FavoriteToolsService.init(this.context).catch(...);

这些 Promise 没有在 loadContent('pages/Index') 前统一等待。因此页面有机会先显示,用户也可能在服务尚未完成初始化时点击收藏、转换或设置。源码做了一些局部保护,例如收藏页用 try/catch 捕获服务未就绪,转换页保存历史时也会静默跳过;但这并不是一个统一的启动状态。

启动回归至少应验证冷启动、热启动、后台恢复、清数据后首次启动和快速点击首屏工具。尤其要确认"页面看见了但服务不可用"时,用户不会得到成功假象。

五、冷启动测试要观察四个时点

建议把冷启动拆成四个可观测时点:

text 复制代码
T0:进程创建
T1:WindowStage 创建
T2:Index 首帧可见
T3:History / Settings / Favorites 全部 ready

当前源码没有公开统一的 AppReady 状态。建议方案是由启动编排器等待必要服务,或者把准备状态写入 AppStorage:

ts 复制代码
// 建议方案,非当前源码
export type StartupState = 'booting' | 'ready' | 'degraded';

await Promise.all([
  HistoryService.init(context),
  SettingsService.init(context),
  FavoriteToolsService.init(context),
]);
AppStorage.setOrCreate<StartupState>('startupState', 'ready');

页面测试就能明确断言:booting 时关键写操作不可用,ready 后才响应;某个非关键服务失败时进入 degraded,而不是无限加载。

六、Ability.test 目前不能证明真实启动

当前 Ability.test.ets 的唯一用例是:

ts 复制代码
it('assertContain', 0, () => {
  let a = 'abc';
  let b = 'b';
  expect(a).assertContain(b);
  expect(a).assertEqual(a);
});

它证明 Hypium 套件可以加载一个简单断言,却没有启动 EntryAbility、等待首页、点击 Tab 或检查崩溃。它不应被计入"启动流程已覆盖"的证据。

这并不意味着要删除模板用例。更合理的做法是保留一个环境冒烟用例,同时新增真实启动测试:确认应用主页面出现、底部导航可见、系统返回可用,日志中没有未捕获异常。

七、空数据回归要覆盖三类页面

项目中至少有三种空数据语义:

  1. HistoryPage暂无记录,提示"使用任意工具后会自动记录"。
  2. HistoryPage 收藏子 Tab:还没有收藏,提示点击星标。
  3. FavoriteToolsPage还没有收藏的工具

空态测试不能只截图文字,还应验证操作约束:

text 复制代码
空历史时"清空"是否隐藏或无害
空筛选结果时编辑模式是否能退出
空收藏时"全部清空"是否不可触发
从空态新增第一条数据后是否立即切换为列表
删除最后一条后是否回到正确空态
重启后空态是否稳定

HistoryPage.allSelected() 已经在过滤结果为空时返回 false,这是一个值得直接单测的边界。

八、历史页的空态文案来自状态函数

真实代码把空态标题和提示集中到两个方法:

ts 复制代码
private emptyTitle(): string {
  if (this.activeTab === 'favorite') return '还没有收藏';
  return '暂无记录';
}

private emptyHint(): string {
  if (this.activeTab === 'favorite') {
    return '点击列表项右侧的 ☆ 即可收藏';
  }
  return '使用任意工具后会自动记录';
}

这比在多个 Builder 分支中重复字符串更容易测试。建议进一步把"过滤类型 → 空态模型"提取成纯函数,让单测直接覆盖全部、转换、计算、收藏四个 Tab。

UI 自动化仍需验证文字真正显示,因为纯函数正确不代表布局没有被底栏遮住,也不代表切换 Tab 后页面用的是最新状态。

九、异常输入要同时检查按钮、结果和历史

ConvertPage 对输入的处理有三条链路:

ts 复制代码
private canConvert(): boolean {
  const value = parseFloat(this.inputText);
  return Number.isFinite(value);
}
ts 复制代码
this.results = convertAll(
  this.group,
  this.fromKey,
  Number.isFinite(value) ? value : NaN,
);
ts 复制代码
if (trimmed.length === 0) return;
const value = parseFloat(trimmed);
if (!Number.isFinite(value)) return;

因此异常输入回归不能只看"转换按钮变灰"。还要确认结果显示为 -、复制与清空按钮状态合理、离开页面不会写历史、再次输入合法值能恢复。

十、parseFloat 带来一个需要测试的语义

parseFloat() 会接受数字前缀。例如输入 "12abc" 时可能得到 12Number.isFinite(12) 为真。当前 canConvert() 因此可能把包含尾随字符的字符串视为可转换。

这不是本文臆测的崩溃,而是 JavaScript/ArkTS 数值解析语义与当前实现组合出的测试点。产品需要明确:

  • 若允许宽松输入,应在 UI 中把标准化后的数值反馈给用户。
  • 若只允许完整数字,应使用更严格的正则或数值解析函数,并为 12abc、多个小数点、单独负号、指数形式和空格建立用例。

建议用例:

ts 复制代码
// 建议方案,非当前源码
expect(parseStrictNumber('12')).assertEqual(12);
expect(parseStrictNumber('12.5')).assertEqual(12.5);
expect(parseStrictNumber('12abc')).assertUndefined();
expect(parseStrictNumber('-')).assertUndefined();

十一、非法输入不能留下旧结果

当合法值改为非法值时,recomputeResults() 会把 NaN 传给 convertAll(),现有单测证明结果格式会变为 -。回归测试还应覆盖状态切换顺序:

text 复制代码
输入 1 → 出现有效结果
清空 → 所有结果变为 -
输入 abc → 按钮禁用,结果仍为 -
输入 2 → 结果恢复
切换源单位 → 结果按 2 重新计算
点击结果行作为新输入 → 输入和结果同步

这类"有效 → 无效 → 有效"比单独输入一个非法值更容易发现缓存、@Watch 和列表刷新问题。

十二、重复点击转换会触发多次异步保存

转换按钮的点击处理为:

ts 复制代码
private handleConvertClick(): void {
  this.recomputeResults();
  this.recordHistoryIfValid(true);
}

按钮只根据输入是否合法启用,没有 savingbusy 状态。用户连续点击会连续调用 HistoryService.add()。服务层存在"相同 kind、type、summary 的记录在 5 秒内更新原记录"的去重逻辑,这是保护层,但页面仍可能弹出多个完成提示,异步返回顺序也值得验证。

重复点击测试应该快速点击 2、5、10 次,然后断言:

text 复制代码
页面不冻结
历史数量符合去重合同
最终记录时间正确
完成提示不会堆叠到不可用
按钮状态能恢复
离开页面不会再额外重复保存

十三、去重合同应在服务层直接单测

HistoryService.add() 的关键逻辑是:若最近记录的 kindtypesummary 相同,且时间差小于 5 秒,则更新已有记录并移到顶部;否则创建新记录。

这段逻辑比页面点击更适合服务测试。建议注入可控时钟:

ts 复制代码
// 建议方案,非当前源码
interface Clock {
  now(): number;
}

const clock = new FakeClock(1_000);
await service.add(payload);
clock.advance(4_000);
await service.add(payload);
expect(service.listRecent(10).length).assertEqual(1);

clock.advance(6_000);
await service.add(payload);
expect(service.listRecent(10).length).assertEqual(2);

如果不注入时钟,测试只能真实等待 5 秒,运行慢且容易抖动。

十四、删除选中存在串行异步窗口

历史批量删除当前逐条等待:

ts 复制代码
for (const id of ids) {
  await service.remove(id);
}
this.selected = [];
this.editMode = false;

remove() 每次都会过滤缓存、全量写回 Preferences 并广播。选中 50 条记录,就会发生 50 次序列化、写入和刷新。在删除期间页面没有显式 deleting 状态,用户可能再次点击。

回归测试应覆盖:

  • 连续点击"删除"是否重复执行。
  • 删除过程中返回页面是否留下半完成状态。
  • 中间一次写入失败时,前面已删和后面未删如何展示。
  • 删除最后一条后空态是否正确。
  • 编辑模式和 selected 是否总能复位。

更好的服务接口是一次 removeMany(ids),单次持久化并返回删除数量。

十五、清空和恢复默认都需要幂等测试

HistoryPage.confirmClearAll()DefaultUnitsPage.resetAll() 都有确认框,成功后显示 Toast。幂等回归应验证:

text 复制代码
取消 → 数据完全不变
确认一次 → 数据恢复目标状态
确认后再次触发 → 不崩溃、不产生错误状态
操作中快速返回 → 重进页面状态正确
写入失败 → 不显示成功 Toast
重启 → 清空/默认状态保持

当前代码的成功 Toast 都放在 Promise then() 中,这一点是正确方向;失败时只记录日志,用户不可见,仍需补测试和交互。

十六、收藏页要测废弃key和快速切换

FavoriteToolsPage.itemsOf() 会解析收藏 JSON,并通过 findToolByKey() 过滤已经不存在的工具 key:

ts 复制代码
set.forEach((key: string) => {
  const item = findToolByKey(key);
  if (item) output.push(item);
});

这是迁移兼容的重要边界。建议单测以下输入:

text 复制代码
空字符串
畸形 JSON
重复 key
不存在 key
一个合法 + 一个废弃 key
大量 key

UI 回归则要快速点击同一星标,检查最终收藏状态与 Preferences 一致。当前 toggle() 是异步写入,页面没有针对同一 key 的请求锁;连续点击可能形成顺序竞争,不能只凭最后一次动画判断持久化结果。

十七、默认单位要测设置、回退和路由

ConvertPage.aboutToAppear() 会读取 defaultUnitOf(),并检查该 key 是否仍存在:

ts 复制代码
const preferred = defaultUnitOf(type, this.defaultUnitsJson);
const exists = this.group.units.find(
  (unit: Unit) => unit.key === preferred,
);
this.fromKey = exists ? preferred : this.group.baseKey;

这条回退链路应覆盖:

  1. 从未设置时使用内置默认单位。
  2. 修改后进入转换页,源单位立即变化。
  3. Preferences 中是废弃 key 时回退 baseKey
  4. 恢复默认后各类型回到预期单位。
  5. 快速切换多个类型不会把 key 写到错误类型。
  6. 设置失败时页面不能假装已保存。

当前页面的 selectUnit() 失败只写日志,UI 可能先因 AppStorage 更新或列表状态出现短暂错觉,需要真机验证实际时序。

十八、回归矩阵应按状态而不是页面数量展开

一个可执行的最小矩阵如下:

场景 初始状态 操作 断言
冷启动 清数据 启动并立刻点工具 不崩溃,核心页可用
空历史 无记录 进入历史 正确空态,无无效编辑
空收藏 无收藏 进入收藏 正确空态
非法输入 12abc 点击转换 按产品合同拒绝或标准化
状态恢复 合法→非法→合法 连续编辑 结果不残留
重复转换 合法输入 快点10次 历史与提示符合去重合同
重复收藏 未收藏 快点10次 最终 UI 与持久化一致
批量删除 多条选中 连续确认 单次执行,状态复位
恢复默认 自定义单位 确认后重启 默认单位持续生效
后台恢复 页面有草稿输入 切后台再返回 不闪退,状态符合设计

矩阵中的每一行都要记录设备、系统版本、包版本、前置数据、操作步骤和结果,避免"我点过了,没问题"的口头结论。

十九、把测试放进发布门禁而不是最后一天

建议流水线分四层:

text 复制代码
提交门禁:
  纯函数单测 + 静态检查

合并门禁:
  服务层持久化测试 + 关键页面 UI 自动化

候选包门禁:
  assembleApp / 签名包 + 安装启动核心流程卸载

上架门禁:
  AppAnalyzer 上架前体检 + DevEco Testing 或 AGC 云测试

华为开发者测试服务把质量覆盖划分为单元测试、UI 测试、专项测试、上架预检和线上监测。对于这个项目,最划算的新增自动化不是再写几十个同类公式样例,而是补上启动、空态、非法输入、重复点击、持久化恢复和删除失败。

还要强调:本文只审阅了测试源码,没有在此发布流程中执行项目 ohosTest、安装包或真机测试,因此不能写"65 个用例全部通过""真机零崩溃"或"审核已通过"。

二十、最小落地顺序与结论

基于当前代码,建议按风险和收益排序:

  1. 保留现有 62 个业务逻辑用例和 1 个环境模板用例,并在 CI 真实执行、保存报告。
  2. 把 Ability 模板用例升级为真实启动冒烟。
  3. 为 HistoryService 的 5 秒去重注入时钟并直接单测。
  4. 为收藏、默认单位和 Preferences 恢复补服务测试。
  5. 建立 ConvertPage 的合法、非法、恢复和重复点击 UI 回归。
  6. 建立历史空态、最后一条删除、批量删除和失败恢复回归。
  7. 给异步按钮增加 saving/deleting/toggling 状态,再验证快速点击。
  8. 对候选包执行安装、冷启动、核心流程、后台恢复和卸载。
  9. 使用 AppAnalyzer、DevEco Testing 或 AGC 云测试补充稳定性、兼容性和上架预检。

"万能转换器"的算法层并不是测试荒地。温度换算、NaN、零利率、错误 JSON、进制非法输入等边界已经被写进源码用例。真正的缺口位于逻辑层之上:服务异步初始化、ArkUI 状态变化、Preferences 持久化、快速重复操作和候选包生命周期。

回归测试的目标也不是让用例数量漂亮,而是让每次修改后都能回答四个问题:能否启动,空数据能否自洽,异常输入能否安全收敛,用户连续操作能否得到唯一且可解释的结果。把这四条做成自动化门禁,才是工具类 HarmonyOS 应用稳定上架和持续迭代的底座。

官方资料可继续参考华为开发者的 HarmonyOS 开发者测试服务入门DevEco Testing 资源与工具开发态快速定位 AppFreeze 冻屏提交 HarmonyOS 应用应用市场审核政策。具体命令、设备能力和测试框架 API,应以项目目标 SDK、DevEco Studio 与 DevEco Testing 对应版本为准。


**AI 辅助声明:**本文在人工核对真实 HarmonyOS 测试源码、Ability 生命周期、服务初始化与输入校验后,使用 AI 辅助整理结构、润色表达并生成配图;测试数量、覆盖范围和未执行边界均以文中列出的源码证据为准。

CSDN-SERIES:ALL-163250228

相关推荐
UnicornIT2 小时前
【HarmonyOS】时间管理类APP:做成“自适应“
ui·华为·harmonyos·鸿蒙
less_121383 小时前
HarmonyOS WPS Open SDK:不落地、水印与功能开关的合规打开策略
华为·harmonyos·wps
OH_TPC14 小时前
HarmonyOS APP开发---“滤镜大师“图像处理App,需要用到这个库
java·图像处理·华为·harmonyos·鸿蒙
2501_9197490317 小时前
华为鸿蒙管理密码APP—小羊密码
华为·harmonyos·鸿蒙
ITUnicorn20 小时前
【HarmonyOS】时间管理类APP:做成“自适应“
harmonyos
笔触狂放20 小时前
第2章 ArkTS(上)
华为·harmonyos·鸿蒙
爱喝水的鱼丶21 小时前
SAP-ABAP:性能优化效果验证与回归测试:优化前后性能对比、业务逻辑兼容性校验方案
性能优化·sap·abap·回归测试·开发交流·交流学习
新元代码21 小时前
探秘鸿蒙南向开发:Hi3861 架构、编译与实战全解析
华为·架构·harmonyos
笔触狂放21 小时前
第1章 初识鸿蒙
华为·harmonyos