news 2026/9/8 1:19:21

【天体运行模拟|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【天体运行模拟|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击

【天体运行模拟|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击

回归测试最容易退化成一张“点过首页、能打开详情”的清单。真正会在上架后暴露的问题,往往发生在第二次动作:快速点两次“查看结果”写出重复记录;启动、暂停、再启动后出现两个计时器;清除数据后页面仍显示旧收藏;Preferences 尚未初始化时,空数据被误判为读取失败;窗口缩放后 Canvas 仍按旧尺寸绘制。

“天体运行模拟”的真实代码包含四个一级 Tab、首页入口、关卡收藏、知识网格、个人中心、N 体模拟计时器、结果记录和本地数据清理。源码已经有一些防护:启动模拟前调用stopTimer(),页面消失时停止计时器;滑块由min/max/step约束;引力计算用最小距离平方避免除零;清除数据前有不可恢复确认。但项目目录中未检索到现成测试用例,因此不能把这些代码结构等同于“已经通过自动化回归”。

本文面向 HarmonyOS 5.0 及以上版本,基于真实页面和数据链,建立从纯逻辑、页面状态、持久化到 release 包的回归测试方案,重点覆盖启动、空数据、异常输入、重复点击与生命周期。

项目基线:应用版本1.0.0,Bundle Name 为com.jiaweikan.one13,目标设备包含 phone、tablet 与 2in1。文中测试代码为可落地的设计示例,实际通过状态必须以本地或设备测试报告为准。

一、回归目标先从用户主链路定义

主链路不是页面列表,而是用户能够完成的闭环:

安装并启动 → 首页选择实验 → 调整参数并启动模拟 → 暂停或查看结果 → 保存实验记录 → 收藏实验 → 返回关卡页复核收藏 → 设置页清除数据 → 重启后确认数据已清除 → 卸载

任何版本只要改动 Canvas、DataStore、路由、主题或布局,都可能影响这条链。回归用例应围绕闭环组织,而不是按文件平均分配。

二、建立测试层级

层级适合验证速度
纯逻辑测试物理计算、格式化、边界映射
服务测试Preferences、记录与清除
页面测试状态、点击、空页、路由
设备冒烟安装、启动、系统栏、生命周期
多设备回归分屏、键鼠、窗口变化最慢

不是所有逻辑都通过 UI 点一遍。物理公式和状态转换放到纯函数后,可以快速覆盖大量边界;系统生命周期和真实绘制必须在设备上验证。

三、为每个用例写四个字段

一个可重跑用例至少包含:

interface RegressionCase { id: string precondition: string action: string expected: string cleanup: string }

例如“清除数据”不能只写“点击清除”。前置要准备收藏、笔记和记录;动作要包含确认;预期要同时检查 UI 与 Preferences;清理要把测试环境恢复为空。

四、冷启动与热启动分开

冷启动验证应用进程不存在时的初始化,热启动验证从后台恢复。两者会触发不同生命周期。

冷启动检查:

  • 首屏不白屏、不闪退;
  • 状态栏和底部导航可读;
  • DataStore.init()完成前页面不误显示错误;
  • 首页 Hero 和 Canvas 正常绘制;
  • 四个 Tab 均可进入;
  • 首次空数据没有残留占位内容。

热启动还要检查模拟计时器、当前 Tab、未保存草稿和页面滚动状态是否符合产品预期。

五、真实 Tabs 状态是第一组回归点

Index.ets维护:

@State currentIndex: number = 0 private tabController: TabsController = new TabsController()

onChange更新索引,首页还通过回调切换到关卡页。至少验证:

  1. 点击四个 Tab,图标与文字选中态一致;
  2. 首页“关卡模式”能切到索引 1;
  3. 连续快速切换不会出现两个 Tab 同时选中;
  4. 从二级页返回后仍回到原 Tab;
  5. 横竖屏或窗口变化不把索引重置为 0。

六、首页空数据不能只验证不崩溃

HomePage默认通过getAllExperiments()获取实验并slice出热门列表。若未来数据源为空,Grid 会自然没有项,但页面是否给出空状态仍要定义。

type ContentState = | 'loading' | 'empty' | 'content' | 'error'

空数组是业务状态,读取失败是错误状态,初始化未完成是 loading。回归测试要分别注入,不能都验成一个空白区域。

七、关卡页空收藏与空实验是两回事

LabPagefavoriteIds默认空数组,空收藏表示所有星标未选中;experiments为空才表示没有关卡。测试应拆开:

  • 有实验、无收藏:列表完整,星标为空;
  • 有收藏:对应 ID 星标选中;
  • 收藏包含不存在 ID:列表不崩溃;
  • 实验为空:显示明确空状态;
  • 分类过滤后为空:提示当前分类无内容,而不是全局加载失败。

这能发现“一个空数组承担多个语义”的问题。

八、Preferences 初始化失败需要可观察

DataStore 多处catch后返回默认值。默认值保护了页面不崩溃,但也可能把读取异常伪装成空数据。测试环境应能注入:

interface FavoritesRepository { load(): Promise<string[]> save(ids: string[]): Promise<boolean> }

页面测试替换成成功、空值、延迟和抛错实现,验证 loading、empty、content、error 四态。没有依赖边界时,异常测试会被静态 DataStore 调用绑住。

九、模拟页启动按钮的真实防重逻辑

源码的启动流程:

private startSimulation(): void { this.isRunning = true this.stopTimer() this.drawCanvas() setTimeout(() => { if (this.isRunning) { this.startTimer() } }, 120) }

startTimer()内部再次调用stopTimer(),这是防止多计时器的重要设计。回归测试要快速点击启动、暂停、启动,并观察:

  • timerId始终只代表一个活动计时器;
  • 暂停后位置不再变化;
  • 再启动从当前状态继续;
  • 页面退出后计时器停止;
  • 延迟 120ms 的回调不会在暂停后重新启动。

十、重复点击不能只看按钮文案

按钮文案从“启动模拟”切为“暂停模拟”,快速点击会在两个状态间切换。真正断言应针对可观察行为:

16ms 时间步只执行一次 body 位置增量不翻倍 学习时长不重复累计 退出后 CPU 不持续占用

UI 文案正确但后台存在两个 interval,仍是严重回归。

十一、页面销毁必须释放资源

真实代码在aboutToDisappear中:

this.stopTimer() this.persistLearningTime()

需要验证三条路径:系统返回、路由跳转到结果页、应用退后台。尤其是跳转结果页前可能仍在运行,离开后 Canvas 更新和 interval 都必须停止。

可以在测试构建中暴露只读诊断值:

interface SimulationDiagnostics { timerActive: boolean stepCount: number lastPersistedAt: number }

release 构建不应保留敏感调试入口。

十二、异常输入从滑块边界开始

参数使用:

Slider({ value: this.paramValues[index], min: param.min, max: param.max, step: param.step })

回归点包括最小值、最大值、非整数步长、快速拖动、窗口缩放中拖动和无障碍调整。每次onChange将值四舍五入到两位,显示又根据step决定小数位,需要验证内部值与显示值一致。

十三、模型层还要防御非 UI 输入

Slider 能约束正常交互,但路由参数、旧数据或测试代码可能传入NaN、Infinity、负质量和越界速度。物理引擎边界不应只依赖组件。

function normalizeMass(value: number): number { if (!Number.isFinite(value)) return 1 return Math.min(5000, Math.max(1, value)) }

测试覆盖NaN、正负 Infinity、0、负数、边界两侧和超大值。归一化规则应与产品参数范围一致。

十四、引力计算已有除零保护

真实算法使用:

const distSq = Math.max(dx * dx + dy * dy, 90) const dist = Math.sqrt(distSq) const force = g / distSq

当两个天体坐标相同,距离平方不会变成 0。这条规则应建立单元测试:

  • 同坐标不产生 NaN 或 Infinity;
  • 极近距离加速度仍为有限值;
  • 两体受力方向相反;
  • 合并后总质量守恒;
  • 合并后速度按动量加权;
  • 半径不超过源码上限 34。

十五、碰撞合并测试要验证不变量

只断言“天体数量减少”不够。应验证:

totalMassAfter = totalMassBefore momentumAfter ≈ momentumBefore one body marked inactive surviving body remains finite black hole type has priority bodyCount equals alive list length

浮点比较使用误差范围,不能直接用字符串或完全相等。

十六、轨迹数组必须有上限

源码中:

if (body.trailX.length > 72) { body.trailX.shift() body.trailY.shift() }

长时间运行测试应确认 X/Y 长度始终一致且不超过 72,隐藏轨迹后不会继续无界增长,重置系统后旧轨迹清除。否则几分钟冒烟正常,长时间运行才出现内存和绘制问题。

十七、越界天体清理

天体超出 Canvas 外扩 200 的范围后标记alive = false,随后过滤数组。测试应覆盖四个方向、刚好边界和边界外一像素,并确认:

  • 删除后bodyCount更新;
  • 绘制不再访问已删除天体;
  • 全部天体离开后页面显示合理状态;
  • 查看结果不会因空数组产生 NaN;
  • 重置可恢复默认系统。

十八、结果页重复点击是高风险点

“查看结果分析”点击处理会:

this.persistRecord() DataStore.incrementExperimentCount() router.pushUrl({ url: resultUrl })

这些异步动作没有等待,也没有submitting防重。快速双击可能写入两条记录、计数两次或发起两个路由跳转。这是当前源码可以识别的回归风险,不能说已经解决。

建议:

@State submittingResult: boolean = false
private async openResult(): Promise<void> { if (this.submittingResult) return this.submittingResult = true try { await this.persistRecord() await DataStore.incrementExperimentCount() await router.pushUrl({ url: resultUrl }) } finally { this.submittingResult = false } }

测试快速点击 2、5、10 次,最终只能新增一条记录和一次计数。

十九、收藏快速点击可能写回旧值

实验页切换收藏时先读取数组、立即更新isFavorite,再保存。两次点击并发可能都读取同一旧数组。回归测试要模拟慢存储并连续点击:

点击序列预期最终状态
一次取反
两次回到初始状态
三次取反
保存失败UI 回滚或明确失败

若最终状态与点击奇偶不一致,需要在仓库层串行整个读改写流程。

二十、清除数据必须防重复确认

设置页已经用AlertDialog告知不可恢复,这是正确的破坏性操作边界。回归用例应验证:

  • 点击取消不改数据;
  • 点击确定清空所有指定键;
  • 连续点击设置入口只出现一个有效对话框;
  • 清除中按钮不可重复执行;
  • 任一写入失败不显示“全部已清除”;
  • 清除后收藏页、记录页和统计快照立即更新;
  • 重启后数据仍为空。

当前多次putString/putNumber顺序执行,但异常被 DataStore 内部吞掉时,页面仍可能 Toast 成功,需通过返回结果改进。

二十一、缓存清理与业务清理分开测

设置页分别提供缓存清理和模拟数据清理。缓存目录为空、目录不可用、文件删除失败、嵌套目录和清理后重新计算大小都应覆盖。

关键断言:清缓存不能删除收藏、笔记和记录;清模拟数据不能误删应用必要资源。名称相近的两个入口最容易在重构中调用错方法。

二十二、路由参数异常

实验页通过expIdexpName接收参数。回归测试传入:

  • 合法实验 ID;
  • 缺失 ID;
  • 空字符串;
  • 未知 ID;
  • 超长名称;
  • 包含特殊字符的名称;
  • 直接进入结果页但缺少统计参数。

页面应使用明确默认实验或显示错误,不应因强制类型转换白屏。默认值必须与产品预期一致。

二十三、返回刷新与旧请求覆盖

LabPageaboutToAppearonPageShow都调用reloadFavorites()。首次显示可能出现两个异步读取。测试让第一次读取延迟更长、返回旧值,第二次快速返回新值,确认旧请求不会最后覆盖新状态。

可用版本号:

private reloadVersion: number = 0
private async reloadFavorites(): Promise<void> { const version = ++this.reloadVersion const ids = await DataStore.loadFavorites() if (version !== this.reloadVersion) return this.favoriteIds = ids }

二十四、窗口变化回归

首页和知识页使用onAreaChange改网格列数。回归宽度至少覆盖599/600/601839/840/841,并连续拖动窗口。

检查:

  • 卡片无重叠或截断;
  • 当前 Tab 不重置;
  • 收藏和模拟状态不丢;
  • Canvas 清理并按新尺寸重绘;
  • 小高度窗口所有操作可滚动到;
  • 2in1 鼠标和键盘仍可操作。

二十五、视觉回归不能只比全屏像素

Canvas 轨道会随时间变化,整页像素比较容易误报。建议分区:

  • 静态导航、卡片和文字做截图差异;
  • Canvas 只检查非空、尺寸、关键颜色和无异常覆盖;
  • 动态数字使用区域遮罩或语义断言;
  • 深浅色分别建立基线;
  • phone、tablet、2in1 保存独立基线。

截图差异必须由人工复核后更新,不能失败就自动覆盖基线。

二十六、构造可重复测试数据

测试不应依赖开发者设备上已有收藏。每个用例前注入固定数据:

interface RegressionFixture { favorites: string[] notes: TestNote[] records: TestRecord[] experimentCount: number learningSeconds: number }

用例结束后恢复或清空。数据 ID、时间戳和排序要固定,避免结果随当前时间变化。

二十七、回归证据结构化保存

每次 release 候选包记录:

版本名 / versionCode 构建时间与提交号 设备型号与系统版本 安装包哈希 用例 ID 与结果 失败截图和日志 修复提交 复测结果

“我刚才点过了”不能支撑发布决策。证据使失败可复现,也避免下一轮重复调查。

二十八、发布门禁

只有以下条件同时满足才进入上架:

  • 本地硬检查与构建通过;
  • 纯逻辑关键边界测试通过;
  • 四 Tab 和主实验闭环通过;
  • 空数据、读取失败和异常参数有明确结果;
  • 重复点击不产生重复写入或路由;
  • 页面退出后无活动计时器;
  • 清除数据后重启仍为空;
  • phone、tablet、2in1 目标布局通过;
  • release 包完成安装、启动、核心流程和卸载;
  • 所有失败有归属,阻断问题为零。

不能用“调试预览正常”替代 release 包冒烟。

二十九、推荐的最小自动化集合

第一阶段不必追求全量 UI 自动化,先落地高价值集合:

  1. resolveSizeClass边界;
  2. 物理步进有限值;
  3. 碰撞质量与动量不变量;
  4. 轨迹长度上限;
  5. 收藏奇偶点击;
  6. 结果提交防重;
  7. DataStore 空值、损坏 JSON 与写入失败;
  8. 清除数据覆盖所有键;
  9. 启停计时器唯一性;
  10. 冷启动主路由冒烟。

这十项能覆盖最容易复发且影响较大的问题。

三十、当前源码的风险清单

风险当前证据回归策略
结果重复提交点击中无防重状态高频点击,断言单条记录
收藏并发覆盖读改写未整体串行慢存储与奇偶点击
清除假成功多次写入无汇总结果注入一次写失败
空数据伪装错误catch 后默认空数组注入异常并检查 error
双生命周期读取两处 reload乱序响应测试
Canvas 尺寸变化动态绘制依赖宽高连续缩放与像素检查
计时器泄漏依赖 stopTimer启停、退出与后台测试

风险清单来自代码结构,不代表线上已发生,也不能虚构测试通过。

三十一、上架前回归清单

  • [ ] 冷启动、热启动和后台恢复通过;
  • [ ] 四个 Tab 与首页快捷切换通过;
  • [ ] loading、empty、content、error 四态可区分;
  • [ ] Slider 最小、最大和步长正确;
  • [ ] NaN、Infinity、负数不会进入物理核心;
  • [ ] 同坐标天体不产生非有限值;
  • [ ] 碰撞质量和动量满足不变量;
  • [ ] 轨迹长度不超过 72;
  • [ ] 启停和退出后只有零个或一个计时器;
  • [ ] 结果和收藏重复点击符合预期;
  • [ ] 清除数据取消、成功和失败路径完整;
  • [ ] 返回刷新不被旧请求覆盖;
  • [ ] 多窗口与三类设备布局可达;
  • [ ] release 包安装、启动、运行和卸载正常。

总结

高质量回归测试不是把页面点一遍,而是把真实代码中的状态边界变成可重复断言:启动要区分冷、热生命周期;空数据要区分加载、错误;异常参数要在模型层归一化;重复点击要验证写入和路由只有一次;页面退出要证明计时器已经释放。

“天体运行模拟”已有不少值得保留的防护,包括计时器启动前主动停止、离页释放、滑块范围、最小距离平方和轨迹长度上限。下一步应把这些隐含规则固化为纯逻辑、页面、持久化和 release 设备四层用例,并优先补齐结果提交、收藏读改写、清除结果与乱序刷新等高风险回归点。

本文唯一标记:CSDN-SERIES:ALL-163201431

AI 辅助声明:本文部分内容由 AI 辅助整理,源码事实、工程边界与风险结论均依据文中所列项目文件复核;未实际执行的测试不标记为通过。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 8:47:22

超高频光刻卡脖子难题,行业专家线下深度交流,一次性吃透工艺痛点

1.光刻是IC制造业中最为重要的一道工艺,占据了芯片制造中大约一半的步骤.光刻占所有成本的三分之一以上。通常可用光刻次数及所需Mask的块数来表示某生产工艺的难易程度。光刻工艺在晶体行业的应用是典型的半导体工艺的延伸发展。2.光刻就是将掩膜版上的几何图形转移到涂有一层…

作者头像 李华
网站建设 2026/9/5 7:13:32

智能体持久化自主行为:从对话到自主工作的架构设计与实践

最近半年&#xff0c;AI 应用开发者的普遍焦虑是&#xff1a;Agent Demo 很好做&#xff0c;但真正想把它放到业务里长期跑&#xff0c;总会发现差一口气。聊天、检索、生成报告&#xff0c;这些单轮能力都不弱&#xff0c;可一旦要求一个 Agent 连续工作一个星期&#xff0c;期…

作者头像 李华
网站建设 2026/9/3 21:57:12

NASA锂电池数据集实战:从容量曲线到SOH与RUL预测

简介&#xff1a;这是一份面向电池管理系统设计、电池寿命预测及机器学习算法研究人员的NASA锂电池试验数据集。数据集中包含B0045至B0048等电池的充放电循环记录&#xff0c;涵盖电压、电流、温度等多维参数随时间的动态变化&#xff0c;可用于分析不同充放电策略对电池性能及…

作者头像 李华
网站建设 2026/9/5 7:01:44

全服世界任务后端实战:Redis并发控制与WebSocket实时推送

最近在聊到原神世界任务的时候&#xff0c;“全世界的玩家&#xff0c;团结一心”这句话很容易让人产生共鸣。从产品设计角度看&#xff0c;这是二游在内容包装上的功力&#xff1b;但从技术角度看&#xff0c;这句话背后其实藏着一个非常经典的分布式协作场景&#xff1a;成千…

作者头像 李华
网站建设 2026/9/4 5:57:51

AI Agent一键生成微信小程序:从需求描述到可运行项目全流程实践

这次我们来看一个很有意思的实践&#xff1a;我把AI Agent和微信小程序开发串成了一个完整的Skill&#xff0c;从你输入一句话需求&#xff0c;到最终在微信开发者工具里跑起来&#xff0c;中间所有环节——需求分析、原型设计、页面结构、逻辑代码、云开发配置、甚至首次运行的…

作者头像 李华