【笔下生辉|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击
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 页面来说,回归测试要先拆出路由、状态、边界保护和可重复操作。当前源码里有几个非常适合做测试断面的点:
| 测试断面 | 真实文件 | 为什么要测 |
|---|---|---|
| 启动与主 Tab | Index.ets |
currentTabIndex、错题角标、安全区、底部导航 |
| 题库详情 | BankDetailPage.ets |
bank 可能为 undefined,章节入口依赖参数 |
| 固定题库入口 | HakkaBankPage.ets |
复用 BankDetailContent,但传入固定 bankId |
| 分类页 | CategoryPage.ets |
按 pageWidth 切换卡片宽度,点击跳搜索 |
| 考试结果页 | ExamResultPage.ets |
JSON 参数解析、得分展示、历史记录防重复 |
这张表的价值在于它把“测试页面”变成了“测试状态变化”。例如 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,只改变本页状态,所以连续点击首页、题库、挑战、错题本、我的,不应该让路由栈增长,也不应该让返回键需要退很多次才能退出。回归测试时可以按下面的顺序操作:
- 启动后连续点击“题库”5 次;
- 连续点击“挑战”5 次;
- 在错题数存在时点击“错题本”,观察角标仍在正确位置;
- 按系统返回,确认行为符合主页面预期,而不是逐层返回到旧 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,records 10 条 |
网格全部显示正确或错误状态 |
total=10,records 3 条 |
前 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
}
这段代码应该被单独纳入回归测试,而不是只看结果页能不能打开。测试步骤可以是:
- 完成一次限时挑战进入结果页;
- 记录考试历史数量;
- 点击“错题解析”进入练习页;
- 返回结果页;
- 再次查看考试历史数量,应保持不变;
- 点击“再来一次”进入新考试,完成后才新增一条历史。
这类重复点击和生命周期回归很常见。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! 的按钮 |
| 固定题库 | 打开 HakkaBankPage |
fixedBankId 生效,展示客家题库 |
| 分类宽屏 | 宽度 >= 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;合集封面保持统一,合集内每篇文章封面保持不同,便于跨平台发布和回读核验。
部分内容由AI辅助生成。
更多推荐



所有评论(0)