☰
【笔下生辉|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击
2026/10/27 9:58:50 网站建设 项目流程

HarmonyOS 5.0+ 应用做回归测试时,最容易被漏掉的不是主路径,而是主路径旁边那些“不太像功能”的边界:启动页跳到首页是否稳定,底部 Tab 被连续点击是否只切状态不制造异常,题库详情页拿不到bankId时是否有空态,分类卡在宽屏下是否仍然排得下,考试结果页收到异常records参数时是否能继续渲染,用户从错题解析返回结果页时是否重复写入考试历史。对离线题库应用来说,这些问题不会触发网络错误,却会直接影响 AppGallery 审核中的启动、导航、布局、空数据和重复操作稳定性。

本文基于「笔下生辉」真实源码复核,包名标记为com.jiaweikang.one17。涉及的文件包括entry/src/main/ets/pages/Index.ets、BankDetailPage.ets、CategoryPage.ets、ExamResultPage.ets、HakkaBankPage.ets,并结合UserDataManager、router、AppStorage和安全区处理来设计回归测试矩阵。本文不会声称源码已经接入自动化测试框架或云测平台,只讨论源码可直接支撑的手工与脚本化回归检查点。

1. 回归测试先从真实路由和状态开始

很多回归清单会写成“启动正常、页面正常、按钮正常”,但这种描述不能指导排查。对 ArkTS 页面来说,回归测试要先拆出路由、状态、边界保护和可重复操作。当前源码里有几个非常适合做测试断面的点:

测试断面真实文件为什么要测
启动与主 TabIndex.etscurrentTabIndex、错题角标、安全区、底部导航
题库详情BankDetailPage.etsbank可能为undefined,章节入口依赖参数
固定题库入口HakkaBankPage.ets复用BankDetailContent,但传入固定bankId
分类页CategoryPage.ets按pageWidth切换卡片宽度,点击跳搜索
考试结果页ExamResultPage.etsJSON 参数解析、得分展示、历史记录防重复

这张表的价值在于它把“测试页面”变成了“测试状态变化”。例如Index不只是一个首页,它承担主 Tab 切换、错题数量角标和底部安全区避让;BankDetailPage不只是题库介绍页,它要同时处理正常题库、找不到题库、章节进度和底部操作;ExamResultPage不只是结果展示页,它还要在aboutToAppear中写入考试历史,并防止返回时重复写入。

2. 启动链路:Splash 到 Index 后先看 AppStorage 和安全区

启动回归的第一条链路是SplashPage -> Index。前文已经复核过入口EntryAbility会初始化currentTabIndex、favoriteTabIndex、topAvoidAreaHeightPx、navigationIndicatorHeightPx,然后加载pages/SplashPage。SplashPage再通过router.replaceUrl({ url: 'pages/Index' })进入主页面。

进入Index.ets后,页面通过@StorageLink读取共享状态:

@StorageLink('currentTabIndex') currentIndex: number = 0 @StorageLink('wrongRecords') wrongRecords: WrongRecord[] = [] @StorageLink('currentBreakpoint') currentBp: string = 'sm' @StorageLink('topAvoidAreaHeightPx') topAvoidAreaHeightPx: number = 0 @StorageLink('navigationIndicatorHeightPx') navigationIndicatorHeightPx: number = 0

这意味着启动回归不应该只看“有没有进入首页”,还要看三个结果。

第一,currentIndex初始是否为 0,首页是否作为首屏内容出现。第二,底部导航高度是否包含bottomSafePadding(),不会贴到系统手势区域。第三,顶部安全区是否通过topSafePadding()让页面内容避开状态栏。

源码里的安全区计算是:

private bottomSafePadding(): number { return Math.max(Sizes.BOTTOM_NAV_MIN_PADDING, this.getUIContext().px2vp(this.navigationIndicatorHeightPx)) } private topSafePadding(): number { return Math.max(Sizes.PADDING_SMALL, this.getUIContext().px2vp(this.topAvoidAreaHeightPx)) }

所以启动测试要覆盖冷启动、返回桌面再进入、横竖屏切换后进入、底部手势导航设备进入。验收点不是截图好不好看,而是首页内容、底部 Tab、角标、状态栏和底部导航区域有没有遮挡或跳动。

3. Tab 重复点击:Index 只改状态,不应该制造路由栈

Index的底部 Tab 使用BottomNavItem(index)构建。点击时执行:

.onClick(() => { this.currentIndex = index })

这是一个很好的回归测试点。它没有调用router.pushUrl,只改变本页状态,所以连续点击首页、题库、挑战、错题本、我的,不应该让路由栈增长,也不应该让返回键需要退很多次才能退出。回归测试时可以按下面的顺序操作:

  1. 启动后连续点击“题库”5 次;
  2. 连续点击“挑战”5 次;
  3. 在错题数存在时点击“错题本”,观察角标仍在正确位置;
  4. 按系统返回,确认行为符合主页面预期,而不是逐层返回到旧 Tab。

错题角标也是重复点击测试的一部分。源码只有在index === 3 && this.wrongRecords.length > 0时显示角标,并把超过 99 的数量显示为99+。这需要覆盖空错题、1 条错题、99 条错题、100 条以上错题四组数据。测试重点是角标文本不溢出、不遮挡图标、不因为重复切 Tab 而重复叠加。

4. 空数据:BankDetailPage 找不到题库时要显示空态

BankDetailPage的核心是可复用组件BankDetailContent。它支持两种进入方式:一种从路由参数读取bankId,另一种由HakkaBankPage这类固定入口传入fixedBankId。源码逻辑是:

aboutToAppear(): void { if (this.fixedBankId.length > 0) { this.bank = getBankById(this.fixedBankId) return } const params = router.getParams() as BankDetailParams | undefined if (params && params.bankId) { this.bank = getBankById(params.bankId) } }

这里必须测两个方向。

正常方向:从题库列表进入,传入有效bankId,页面展示封面、统计、章节、底部“随机练习”和“限时挑战”按钮。固定入口方向:HakkaBankPage传入fixedBankId: 'b_hakka',不依赖路由参数也能展示客家题库详情。

异常方向:手动构造无效bankId或不带参数进入BankDetailPage。源码在this.bank === undefined时显示空态图片和“未找到题库”文案,而不是继续访问this.bank!.id。这个空态非常关键,因为下面的HeroCard、章节列表和底部按钮大量依赖this.bank!,只有外层if把它们隔离开,页面才不会空指针崩溃。

回归测试的验收点可以写成:

输入预期
bankId = b_hakka展示题库详情和章节列表
fixedBankId = b_hakka固定入口可正常打开
bankId = not_exists显示空态,不显示底部练习按钮
无参数进入显示空态,不崩溃

这类空数据测试比主路径更重要。主路径通常在开发时天天被打开,空态只有在路由参数异常、数据变更、入口改造或深链失败时才会暴露。

5. 响应式回归:CategoryPage 和 BankDetailPage 都依赖 pageWidth

CategoryPage用pageWidth决定分类卡片宽度:

private itemWidth(): string { return this.pageWidth >= 720 ? '24%' : '48%' }

页面通过onAreaChange更新pageWidth,因此回归测试要覆盖手机窄屏、平板宽屏、小窗缩放和横屏。窄屏下每行大约两张卡片,宽屏下每行约四张卡片。测试点包括卡片文字maxLines(1)和textOverflow是否生效,题型说明是否被截断到不可理解,点击分类卡是否能带着categoryType和categoryName跳到SearchPage。

BankDetailPage也有响应式逻辑:

private useWideLayout(): boolean { return this.currentBp === 'lg' && this.pageWidth >= 700 }

宽屏时页面拆成左右两列,左侧放封面、摘要和资料卡,右侧放重点和章节列表;窄屏时使用单列 Scroll。回归测试不能只在手机竖屏验收,还要在宽度超过 700vp 且断点为lg的场景下检查:左右两列是否同时可滚动,章节列表是否不被底部按钮遮挡,封面图是否保持比例,底部按钮是否仍有安全区 padding。

这些检查直接对应 AppGallery 的布局审核风险:折叠、旋转、小窗、平板和 2in1 下,组件不能错位、遮挡、截断或不可点击。

6. 异常输入:ExamResultPage 的 records 解析失败也要能渲染

考试结果页通过路由参数接收bankId、score、total、correct、durationSec和records。其中records是 JSON 字符串,源码用try/catch处理:

try { this.records = JSON.parse(params.records) as AnswerRecord[] } catch (_) {}

这说明回归测试必须覆盖异常records。如果records不是合法 JSON,页面不应该白屏,也不应该阻断得分、用时、正确率和错题数量展示。答题网格由makeAnswerGrid()根据total构造,如果records.length不足,则剩余题目状态为empty:

private makeAnswerGrid(): GridItem[] { const result: GridItem[] = [] for (let i = 0; i < this.total; i++) { let state: string = 'empty' if (i < this.records.length) { state = this.records[i].correct ? 'correct' : 'wrong' } result.push({ index: i + 1, state }) } return result }

这里至少要测四组参数:

参数组合预期
total=10,records10 条网格全部显示正确或错误状态
total=10,records3 条前 3 题显示记录,后 7 题显示未答
total=0,records=[]正确率计算不除以 0,页面可渲染
records='broken-json'捕获异常,网格按空记录处理

源码中正确率使用Math.max(this.total, 1)避免除以 0,错题数量使用Math.max(this.total - this.correct, 0)避免负数显示。测试时要确认这些保护不会因为异常输入被绕开。

7. 重复写入:historySaved 是考试结果页的关键保护

ExamResultPage在aboutToAppear中保存考试历史。这个生命周期在页面首次进入时触发,也可能在从错题解析页返回时再次触发。如果不加保护,用户看一次结果、进一次解析、再返回,就可能写入多条相同考试历史。

源码用historySaved防重复:

if (!this.historySaved && this.total > 0) { this.examHistory = UserDataManager.addExamHistory( this.examHistory, this.bankId, this.score, this.total, this.correct, this.durationSec) this.historySaved = true }

这段代码应该被单独纳入回归测试,而不是只看结果页能不能打开。测试步骤可以是:

  1. 完成一次限时挑战进入结果页;
  2. 记录考试历史数量;
  3. 点击“错题解析”进入练习页;
  4. 返回结果页;
  5. 再次查看考试历史数量,应保持不变;
  6. 点击“再来一次”进入新考试,完成后才新增一条历史。

这类重复点击和生命周期回归很常见。ArkUI 页面返回、replaceUrl、pushUrl、aboutToAppear组合在一起时,数据写入不能只靠“用户不会这样操作”来保证。historySaved是当前源码中真实存在的防线。

8. 路由动作:pushUrl 与 replaceUrl 的差异要测出来

源码里有两类路由动作。进入详情、练习、设置、搜索,多数使用router.pushUrl;从考试结果页“再来一次”使用router.replaceUrl。这不是写法差异,而是用户体验差异。

pushUrl会把新页面压入栈,适合从列表进入详情、从结果进入错题解析,因为用户通常希望能返回上一个上下文。replaceUrl会替换当前页,适合“再来一次”这种重新开始流程,避免用户返回到旧结果页再重复触发操作。

回归测试需要按按钮逐个确认:

页面动作预期
CategoryPage分类卡pushUrl到SearchPage返回后仍在分类页
BankDetailPage章节按钮pushUrl到PracticePage返回后仍在题库详情
BankDetailPage随机练习pushUrl到PracticePage返回后仍在题库详情
ExamResultPage错题解析pushUrl到PracticePage返回后仍在结果页且不重复写历史
ExamResultPage再来一次replaceUrl到PracticePage不把旧结果页留在当前返回路径中

这些检查能发现很多“功能能用但返回体验错”的问题。对上架审核来说,返回路径也是稳定性的一部分,不能只测按钮点击瞬间。

9. 回归矩阵:把边界写成可执行清单

基于当前源码,可以把回归矩阵整理成下面这组最小清单。

类别操作验收点
启动冷启动进入应用Splash 跳 Index,首页显示,安全区正常
主 Tab连续切换 5 个 Tab不崩溃、不堆路由、角标位置稳定
空题库无效bankId进入详情显示空态,不显示依赖bank!的按钮
固定题库打开HakkaBankPagefixedBankId生效,展示客家题库
分类宽屏宽度 >= 720分类卡约四列,文字不溢出
分类窄屏手机竖屏分类卡两列,说明区域可滚动
结果异常records非法 JSON捕获异常,页面仍显示得分和空网格
结果空数据total=0正确率不除以 0,不出现负错题
重复返回结果页进解析再返回examHistory不重复写入
底部安全区手势导航设备底部按钮不进入系统手势区

如果要把这套清单接入脚本化测试,可以先不追求覆盖所有 UI 像素,而是优先检查路由参数、状态数量、关键文本和数据写入次数。自动化不足的部分,用固定截图和手工复核补齐。

10. 发布前建议:回归测试要和审核风险对齐

当前「笔下生辉」是离线题库应用,回归测试的重点不在网络超时或服务器异常,而在本地状态、路由参数、布局适配和重复操作。发布前至少要把下面四类风险跑完。

第一类是兼容性风险:启动、首页、Tab、详情、练习、结果页、返回路径必须稳定,不能闪退、白屏或无法返回。

第二类是布局风险:Index的底部导航、BankDetailPage的宽窄布局、CategoryPage的卡片列数、ExamResultPage的底部按钮,都要在手机、平板、2in1、小窗和横屏下检查。

第三类是数据风险:本地 Preferences 写入不能因为返回生命周期重复执行,清空或异常参数不能让页面进入不可恢复状态。

第四类是交互风险:重复点击 Tab、连续点章节、结果页重复进入解析、再来一次、返回键,都要确认不会制造多余路由或重复记录。

11. 小结:回归测试不是补表格,而是保护真实源码边界

「笔下生辉」当前源码已经提供了几个明确的回归保护点:Index用AppStorage和安全区处理启动后的主框架,BankDetailPage在bank === undefined时显示空态,CategoryPage通过pageWidth控制分类卡响应式布局,ExamResultPage用try/catch处理异常records,用Math.max避免极端数据计算异常,并用historySaved避免考试历史重复写入。

这些都是真实代码可以复核的测试依据。回归测试应该围绕它们设计,而不是只写“功能正常”。当测试清单能覆盖启动、空数据、异常输入、重复点击和多设备布局时,应用在后续改题库、改导航、改结果页或改设置项时,才不容易把稳定性问题带到发布包里。

本篇文章的封面使用当前文章独立的media/cover.png,同一软件合集继续复用应用级collection-cover.png;合集封面保持统一,合集内每篇文章封面保持不同,便于跨平台发布和回读核验。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询