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

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

回归测试封面

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 会初始化 currentTabIndexfavoriteTabIndextopAvoidAreaHeightPxnavigationIndicatorHeightPx,然后加载 pages/SplashPageSplashPage 再通过 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

CategoryPagepageWidth 决定分类卡片宽度:

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

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

BankDetailPage 也有响应式逻辑:

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

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

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

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

考试结果页通过路由参数接收 bankIdscoretotalcorrectdurationSecrecords。其中 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=10records 10 条 网格全部显示正确或错误状态
total=10records 3 条 前 3 题显示记录,后 7 题显示未答
total=0records=[] 正确率计算不除以 0,页面可渲染
records='broken-json' 捕获异常,网格按空记录处理

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

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

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

源码用 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 页面返回、replaceUrlpushUrlaboutToAppear 组合在一起时,数据写入不能只靠“用户不会这样操作”来保证。historySaved 是当前源码中真实存在的防线。

回归测试职责结构

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

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

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

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

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

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

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. 小结:回归测试不是补表格,而是保护真实源码边界

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

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

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

部分内容由AI辅助生成。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐