【中国方言题库|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击

发布时间:2026/8/28 17:23:41
【中国方言题库|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击 HarmonyOS 应用的回归测试不能只验证“首页能打开”。真实故障往往出现在启动数据损坏、路由参数缺失、空题库、考试记录 JSON 非法、重复点击交卷、计时器离开页面后仍执行、窗口切换导致布局分支失效等边界。尤其是本地题库应用主流程不依赖网络稳定性重点更集中在状态初始化、页面路由、持久化、交互幂等和多设备布局上。中国方言题库工程已经包含 Hypium 的src/test与src/ohosTest目录但现有测试仍是模板示例只断言字符串abc包含b没有覆盖题库、练习、收藏、进度、考试和页面路由。业务源码本身则提供了不少可测试边界例如题库不存在时显示“未找到题库”、错题为空时显示空状态、JSON 解析失败后回退空数组、选项重复点击被showAnalysis拦截、启动页离开时清理定时器。文章会严格区分“源码已有防护”和“测试已经验证”没有真实测试用例的地方不会写成已通过。本文面向 HarmonyOS 5.0 及以上版本基于启动页、题库详情、分类页、练习页、考试结果页、主框架和现有 Hypium 脚手架设计一套可落地的回归测试矩阵。本文唯一核验标记先验证状态边界再验证页面结果最后验证重复操作不会产生第二次副作用。一、先审计现有测试而不是假设测试存在工程包含entry/src/test/LocalUnit.test.ets entry/src/test/List.test.ets entry/src/ohosTest/ets/test/Ability.test.ets entry/src/ohosTest/ets/test/List.test.ets librarya/src/test/... librarya/src/ohosTest/... libraryb/src/test/... libraryb/src/ohosTest/...oh-package.json5也声明了devDependencies: { ohos/hypium: 1.0.25, ohos/hamock: 1.0.0 }说明工程具备编写本地单元测试与设备测试的基础。但 entry 当前唯一示例是it(assertContain, 0, () { let a abc let b b expect(a).assertContain(b) expect(a).assertEqual(a) })ohosTest 的 Ability 示例也是相同断言只多了一条 hilog。它只能证明测试模板可以被组织不能证明任何业务回归已经覆盖。二、回归测试应该按风险链路组织对当前应用最有价值的链路是Ability 启动 - 初始化 Preferences 与 AppStorage - 启动页计时跳转 - Index 选择 Tab - 题库详情加载 - PracticePage 组题和答题 - 结果页保存考试历史 - 设置页清除本地数据测试不应按“每个文件写一个用例”机械分配而应围绕用户可感知结果和数据副作用展开。一次回归至少要覆盖正常路径、空状态、异常输入和重复操作。三、启动链路需要验证什么EntryAbility.onCreate()会执行三类初始化设置浅色模式 UserDataManager.init(context) 初始化 AppStorage 的 Tab 和安全区状态 注册 BreakpointSystemonWindowStageCreate()获取主窗口、监听避让区变化并加载windowStage.loadContent(pages/SplashPage, ...)启动回归至少要检查Preferences 为空时能启动 Preferences 包含合法历史数据时能恢复 某个 JSON 键损坏时不会白屏 无法读取避让区时使用 0 回退 SplashPage 能在约 2 秒后 replace 到 Index 离开 SplashPage 后计时器被清理 重复创建/销毁 Ability 不残留媒体查询监听当前UserDataManager.init()用一个大try/catch包住全部键读取。任意一个 JSON 解析失败catch 会把所有共享数据恢复为默认值。这保证启动不崩溃但可能让本次运行丢失其他仍然完好的数据。测试应明确验证这个现状并推动后续按键隔离解析。四、SplashPage 的定时器回归启动页在aboutToAppear()中调用this.timerId setTimeout(() { router.replaceUrl({ url: pages/Index }) }, 2000)在aboutToDisappear()中if (this.timerId ! -1) clearTimeout(this.timerId)需要覆盖两个场景。第一正常停留进入 SplashPage 等待计时完成 断言只发生一次 replaceUrl 目标为 pages/Index第二提前离开进入 SplashPage 在 2 秒前触发 disappear 推进虚拟时间 断言不再发生 replaceUrl如果测试框架难以替换全局定时器可以把“延时跳转”封装到可注入的导航/调度服务再做纯单元测试设备测试只保留一条端到端验证。五、题库详情页的空输入BankDetailContent从固定 ID 或路由参数加载题库const params router.getParams() if (params params.bankId) { this.bank getBankById(params.bankId) }当bank为undefined时页面明确渲染空状态图片 “未找到题库”应至少覆盖params 为 undefined params 不含 bankId bankId 为空字符串 bankId 为不存在的值 fixedBankId 合法 fixedBankId 不合法测试结果不是“代码没有抛异常”就结束还要断言 TopBar 仍存在、空状态文本可见、底部随机练习与模拟考试按钮不应在无题库时出现。六、HakkaBankPage 的复用回归HakkaBankPage只有一层包装BankDetailContent({ fixedBankId: b_hakka })它的风险不在复杂逻辑而在“固定入口是否真的映射到正确题库”。测试应断言进入 HakkaBankPage 显示客家话题库名称 章节来自 b_hakka 点击章节传递 bankIdb_hakka 随机练习和模拟考试也携带 b_hakka这类地区专属入口适合做参数化测试避免每个页面复制一套测试代码。七、分类页需要做边界宽度回归CategoryPage.itemWidth()的规则是return this.pageWidth 720 ? 24% : 48%测试应覆盖精确边界719vp - 48% 720vp - 24% 721vp - 24%同时模拟onAreaChange的newArea.width0 或负值不更新 pageWidth 合法数字更新 无法转成数字时不应破坏已有宽度现有代码只判断width 0。Number(abc)会得到 NaN条件为 false可以保留旧值。测试应固定这一回退行为防止后续重构直接赋值导致卡片布局异常。八、Index 的布局分支必须纳入回归当前Index的条件是if (currentBp sm || currentBp md || currentBp lg) { // 底部导航 } else { // 侧边导航 }由于合法断点只有sm/md/lg侧边导航不可达。回归测试应把产品预期写成断言例如sm 显示底部导航 md 显示底部或侧边导航按产品策略 lg 显示侧边导航当前代码执行“lg 显示侧边导航”会失败这是一条有价值的红灯测试。回归测试不是为了全部变绿而回避问题而是先把预期固定再修正实现。九、空收藏、空笔记和空错题FavoritePage对三类数据分别判断favRecords.length 0 noteRecords.length 0 wrongRecords.length 0错题空状态文案是暂无错题记录 答错的题会自动收录到这里测试应分别清空 AppStorage 三个数组然后切换对应 Tab断言空状态图片存在 标题与说明匹配当前 Tab 列表项为 0 没有访问 records[0] 导致异常再写一组从空到有、从有到空的状态转换收藏一道题后列表出现取消收藏后恢复空状态保存一条笔记后显示保存空白笔记后删除错题答对后记录移除。十、PracticePage 的无题目回退练习页根据 mode 和路由参数组题wrongAnalysis - 从 records 找错题 wrong - 从 wrongRecords 找题 chapter - 按章节取题 exam - 取最多 20 题 random - 洗牌如果非错题解析模式下组题结果为空会回退if (this.questions.length 0 this.mode ! wrongAnalysis) { this.questions getQuestions(params.bankId) }需要验证不存在章节但题库有效 - 回退整库 错题记录引用已删除题目 - 过滤无效记录 bankId 不存在 - questions 仍为空且页面不崩溃 wrongAnalysis 无错题 - 显示 0/0 并可返回 exam 题库不足 20 题 - 不越界空题库下不能触发currentQ()!的强制非空路径。测试应点击底部工具栏、答题卡和下一题确认所有动作都有空值保护。十一、异常 JSON 的回归练习页错题解析try { examRecords JSON.parse(params.records) } catch (_) { examRecords [] }考试结果页也在解析 records 时使用try/catch失败后保留空数组。应使用一组异常输入undefined 空字符串 普通文本 不完整 JSON {} null [1,2,3] 缺少 questionId 的对象数组 selected 类型错误当前实现只防止语法解析异常没有运行时结构校验。[1,2,3]是合法 JSON但后续读取record.correct、record.questionId时语义不成立。测试会暴露这个边界推动增加类型守卫或 normalizer。十二、选项重复点击已有一层幂等保护selectOption()开头是if (this.mode wrongAnalysis) return if (this.showAnalysis) return第一次选择后立即this.selectedKey key this.showAnalysis true this.records.push(...)因此连续点击同一选项或不同选项第二次调用会因showAnalysis返回不会重复写入答题记录。这条保护值得用单元测试固定初始 records 长度 0 调用 selectOption(A) 再调用 selectOption(B) records 长度仍为 1 selectedKey 保持 A 错题记录最多新增一次文章不能说它已经通过因为现有 Hypium 测试没有该用例只能说源码存在可验证的幂等条件。十三、自动下一题与手动点击的竞态当设置autoNextQuestiontrue且不是考试模式选项提交后会启动延时setTimeout(() { if (this.showAnalysis) { this.goNext() } }, ...)在延时到达前用户仍可能手动点击“下一题”。手动goNext()会把showAnalysis设为 false因此延时回调检查后通常不会再前进一次。测试应模拟答题触发自动下一题 延时到达前手动下一题 推进时间 currentIdx 只增加 1还要测试用户切到答题卡中的另一题后旧延时是否会错误前进。仅检查showAnalysis可能不足以绑定“这是哪一道题的定时任务”更稳的方式是记录题目 ID 或取消旧 timer。十四、最后一题重复点击是高风险路径底部“完成/交卷”按钮满足条件时直接调用goNext()。最后一题中goNext()会计算 correctCount 更新题库进度 更新章节进度 replace 到结果页或 router.back当前没有isSubmitting或isNavigating标志。如果用户在路由切换完成前快速点击两次第二次可能再次更新进度。是否能在真实设备上稳定复现取决于 UI 事件和路由时序但代码没有显式幂等保证。回归测试应构造currentIdx 位于最后一题 showAnalysistrue 快速触发两次完成 UserDataManager.updateProgress 只调用一次 replaceUrl 或 back 只调用一次如果测试失败最小修复是if (this.isSubmitting) return this.isSubmitting true然后在失败回退路径恢复状态按钮同时显示禁用视觉。十五、考试超时与手动交卷竞态计时器每秒执行if (mode exam examRemainSec() 0) { autoSubmitExam() }autoSubmitExam()首先清除 interval并把timerId设为 -1这能避免下一秒再次自动提交。但超时瞬间用户也可能手动点击交卷两条路径都可能更新进度并跳转。测试应让剩余时间为 0同时触发计时器回调 手动交卷点击然后断言进度、考试历史和导航各发生一次。自动提交与手动提交最好最终汇聚到同一个带幂等锁的submitExam()。十六、ExamResultPage 的重复出现结果页aboutToAppear()每次都会执行this.examHistory UserDataManager.addExamHistory(...)正常流程通过replaceUrl进入一次通常只追加一条。但如果页面因为生命周期重入、导航恢复或测试重复调用aboutToAppear()可能重复保存同一场考试。测试可以直接调用两次初始化逻辑检查历史记录是否增加两条。若产品预期一场考试只保存一次需要给考试结果携带唯一 attemptId或在提交考试时保存历史结果页只负责展示。十七、答题卡索引需要防越界SheetDot(idx)点击时直接this.currentIdx idx const record this.records.find( r r.questionId this.questions[idx].id )正常 UI 的 idx 来自 questions 遍历因此范围合法。但单元测试和后续重构可以验证idx0 idxquestions.length-1 idx-1 idxquestions.length questions 在弹层打开后被替换为空当前负数或越界会访问undefined.id。这不一定能由正常 UI 触发却是组件方法缺乏边界保护的证据。可在方法开头增加if (idx 0 || idx this.questions.length) return十八、收藏按钮为什么要测偶数次点击收藏使用toggleFavorite()每次点击在存在与不存在之间切换。快速双击的结果会回到初始状态未收藏 - 收藏 - 未收藏从纯逻辑看是正确的但用户可能认为自己只点击了一次。测试重点包括单击后收藏记录增加 1 再单击后删除 连续奇数次和偶数次的最终状态 每次持久化结果与 AppStorage 相同 同一题不会出现重复记录如果产品需要防误双击可增加短时间防抖如果允许快速切换则测试应固定最终幂等结果。十九、设置页删除操作的重复点击清除全部学习数据使用两次点击确认第一次 - pendingCleartrue只提示 第二次 - 执行清除测试应覆盖第一次不删除 点击其他设置后 pendingClear 被取消 第二次删除所有目标数组 第三次点击重新进入确认而不是再次清除 持久化失败时不能错误显示成功当前持久化异常被静默吞掉最后一项需要先改造结果返回才能可靠验证。二十、纯函数优先做本地单元测试适合src/test的逻辑包括QuestionUtils.formatTime QuestionUtils.calcScore QuestionUtils.correctCount 题库筛选与排序 进度计算 分类宽度阈值 答题记录 normalizer 重复提交状态机这些测试不依赖 UIAbility、窗口或真实 Preferences运行快适合每次提交执行。UserDataManager当前把 Preferences 静态对象和业务更新放在同一个类中测试时不容易替换存储。可以把“计算新数组”拆成纯函数把落盘封装成 repository再分别测试。二十一、页面与设备行为放进 ohosTest适合 ohosTest 的路径包括应用启动到 Index Tab 切换 题库详情空状态 练习选项点击与解析显示 收藏、笔记和错题跨页面同步 考试完成到结果页 设置页清除数据 安全区和窗口尺寸变化 返回导航设备测试数量不需要覆盖所有数据组合。把复杂数据排列留给单元测试ohosTest 重点验证 ArkUI 渲染、路由、生命周期和系统交互。二十二、测试数据必须可重复回归测试最怕依赖上一次执行残留。每个用例前应建立明确状态清空或使用独立 Preferences 测试库 写入固定收藏、错题和进度 固定当前时间或注入 Clock 固定随机题序或注入 Random 重置 AppStorage 清理计时器、TTS 引擎和路由桩考试题目按小时轮换pickHourlyExamQuestions()可能依赖时间。若不固定时钟同一测试在整点前后会得到不同题序。应把当前时间作为参数或服务注入。二十三、建立四层回归矩阵启动层首次安装 有历史数据 损坏数据 Ability 重建 窗口避让区异常数据层空数组 单条记录 重复记录 非法 JSON 结构合法但字段错误 持久化失败页面层无路由参数 无效 bankId 不存在章节 空错题 结果记录为空 长文本与小窗口交互层选项连点 自动与手动下一题竞态 最后一题重复完成 超时与手动交卷竞态 收藏双击 删除确认重复点击二十四、发布前最小烟测自动化用例之外release 包至少完成一次安装 冷启动 进入首页 切换五个 Tab 进入题库详情 完成一组练习 完成一次模拟考试 查看收藏、错题和统计 退出并重启验证恢复 清除学习数据 横竖屏或调整窗口 卸载要记录真实结果不应因为代码里有空状态和 try/catch 就推断运行稳定。本文没有执行设备安装与 UI 自动化因此只提供基于源码的测试设计不报告任何未运行的通过结果。二十五、测试命名要描述行为不要继续使用assertContain这类模板名。建议it(loadsDefaultStateWhenPreferencesIsEmpty, 0, ...) it(showsEmptyStateForUnknownBankId, 0, ...) it(ignoresSecondOptionTapAfterAnswer, 0, ...) it(submitsFinalQuestionOnlyOnce, 0, ...) it(fallsBackToEmptyRecordsForInvalidJson, 0, ...)看到失败名称就能知道哪条产品契约被破坏。二十六、回归测试的最小落地顺序第一批先覆盖最危险且易测试的纯逻辑分数计算 进度更新 收藏切换 笔记空白删除 错题去重 非法记录归一化第二批覆盖启动和路由输入Preferences 损坏 无效 bankId 空错题解析 无效 records JSON第三批覆盖竞态选项连点 最后一题重复完成 超时与手动交卷 自动下一题与手动下一题第四批完成设备烟测和多窗口适配。这样能用较少用例先守住最容易造成数据重复和崩溃的路径。二十七、结语中国方言题库已经具备空状态、JSON 解析回退、选项重复点击拦截、计时器销毁和本地数据清除等防护但现有 Hypium 文件仍是默认模板不能据此宣称业务测试已经完成。真正的回归体系要把源码里的边界变成可执行断言并优先验证最后一题、超时交卷、页面重入和持久化失败这些会产生重复副作用的路径。整套方法可以概括为先验证状态边界再验证页面结果最后验证重复操作不会产生第二次副作用。当启动、空数据、异常输入和重复点击都有稳定断言HarmonyOS 应用的兼容性与运行稳定性才从“人工感觉没问题”变成可复核的工程证据。AI 辅助声明本文在结构整理和语言润色环节使用了 AI 辅助现有测试覆盖、空状态、JSON 回退、计时器、重复点击与导航行为均依据中国方言题库真实 ArkTS 与 Hypium 文件复核。