【时光清单19】HarmonyOS ArkTS 回归测试实战覆盖启动、空数据、异常输入和重复点击**本轮证据边界**本文基于D:\huawei\one8时光清单当前源码、测试目录与项目错误记录做静态复核。当前仓库可见 6 个模板it()声明但本轮没有执行测试、构建、安装、模拟器、真机、故障注入或远端发布源码声明绝不等于实际通过。阅读约定“当前事实”来自文中列出的真实文件“历史证据”仅指既有项目记录日期解析器、保存互斥、可替换存储、业务测试套件和设备矩阵均为建议实现不代表当前工程已经落地。一个 HarmonyOS 应用能够编译并不代表它已经具备可上架的稳定性。真正容易在回归阶段暴露的问题往往不是某个 API 不会调用而是状态组合没有被测试首次安装没有任何数据时卡片显示什么Preferences 初始化失败后首页会不会白屏日期输入为无效字符串时是否把NaN写入仓库用户连续点击两次“保存”会不会创建两条纪念日备份文件只有version和空数组时能否被错误地接受。时光清单的真实源码已经包含entry/src/test与entry/src/ohosTest但其中用例仍是工程模板自带的assertContain。这只能证明源码中存在测试入口和一个简单断言不能证明启动链路、Navigation、服务卡片、持久化或用户输入经受过回归。与此同时业务代码里已经出现了很适合转成测试契约的边界EntryAbility同步初始化后再异步 hydrateCountdownFormAbility在空数据和异常时返回默认卡片AddView对空标题直接返回但没有日期有效性判断BackupService只校验两个字段保存按钮没有处理中互斥状态。本文面向当前 HarmonyOS 工程从这些真实代码出发设计一套分层回归策略。重点不是堆用例数量而是让每个高风险行为都有明确输入、可观察结果、失败判据和清理方法。文中的测试代码是针对当前结构给出的落地建议除明确引用的现有模板外不会把建议用例描述成项目已经执行通过。本文将解决如何判断现有 Hypium 用例是否真的覆盖业务。启动同步初始化、异步 hydrate 与首帧状态怎样测试。空仓库、已删除选择项和异常数据下桌面卡片应如何兜底。空标题、非法日期、损坏备份和超长文本怎样构造。重复点击如何验证幂等、互斥与数据版本通知。单元测试、ArkUI 自动化、真机冒烟和上架预检如何分工。本文唯一标记CSDN-SERIES:ALL-163210362零、事实、历史证据与建议实现先分开本文把三类信息分开记录避免把测试计划写成测试结果。当前事实本轮逐文件复核D:\huawei\one8。仓库可见 6 个it()声明其中 entry、librarya、libraryb 各有一个本地模板和一个设备模板全部仍是assertContain。这只是源码声明数量。历史证据PROJECT_ERRORS.md记录 2026-05-20 曾修复情绪背景恢复、跨页刷新、主题对比度和功能数据同步并记录当时assembleHap成功。该记录不等于本轮重新构建或回归。建议实现文中的日期解析器、可替换存储、保存互斥、业务 Hypium 套件、故障注入和候选包冒烟均是下一步方案当前工程尚未因此自动获得这些能力。本轮没有执行 Hypium、构建、安装、模拟器、真机或故障注入因此不会给出“通过数”“覆盖率”“启动耗时”或设备结论。后文凡是出现“应”“建议”“验收”都表示待实现或待执行不表示已经完成。一、先承认现有测试只验证了“框架能跑”项目当前本地测试入口是import localUnitTest from ./LocalUnit.test; export default function testsuite() { localUnitTest(); }实际用例来自默认模板it(assertContain, 0, () { let a abc; let b b; expect(a).assertContain(b); expect(a).assertEqual(a); });ohosTest中的 Ability 测试同样只断言字符串包含关系。这个现状并不意味着测试工程无价值目录、Hypium 导入、测试套件入口和设备测试模块都已生成补业务用例的基础存在。但覆盖结论必须诚实当前源码能证明当前源码不能证明三个模块均存在本地与设备测试目录任一测试已经编译、加载或运行六个 List 入口会聚合对应模板函数EntryAbility 完成真实安装启动六个模板文件共声明 6 个it()Preferences 读写与恢复正确模板导入 Hypium 并写有基础断言页面、路由、卡片和重复点击通过回归因此第一步不是宣布“已有自动化测试”而是把模板用例替换为业务契约。每增加一条业务用例都应回答它保护的是哪条真实源码路径失败时能否告诉开发者具体哪里变了二、按发布风险分层而不是按页面数量分层如果按页面逐个写测试很快会得到大量“点击按钮后文字存在”的脆弱脚本。更稳的分层方式是层级主要对象适合验证纯逻辑单元测试日期计算、默认值、格式校验快速、确定、无需设备Repository/Service 测试Preferences、备份、合并、幂等数据边界与故障注入ArkUI/卡片测试页面状态、输入、按钮、布局交互和渲染结果UIAbility/设备测试启动、路由、前后台、配置变化生命周期与系统能力发布候选包冒烟安装、启动、核心流、卸载AppGallery Guideline 3.1同一风险可以跨层验证。例如“无数据启动”单元层验证仓库默认返回[]。卡片层验证getFormData()返回标题“时光清单”和天数0。ArkUI 层验证首页空状态可操作。设备层验证首次安装启动不闪退、不白屏。分层后UI 自动化不必承担全部逻辑验证纯函数也不会被误当成真实设备证据。三、启动链路同步首帧与异步恢复要分别验EntryAbility.onCreate()的实际顺序是onCreate( want: Want, launchParam: AbilityConstant.LaunchParam ): void { try { DataStore.getInstance().initSync(this.context); const mood DataStore.getInstance() .getJsonSyncstring( DataKeys.MOOD_BACKGROUND, auto ); this.setStorageString( StateKeys.MOOD_BACKGROUND, mood ); } catch (_e) {} AppStore.bootstrap(this.context); DataStore.getInstance().init(this.context); this.hydratePersistentState(); }这段代码解决了一个真实风险先同步读取情绪背景避免 UI 首帧使用默认值后闪回随后再执行异步 hydrate补齐持久化状态。回归测试不能只验证“最后值正确”还要验证首帧和最终帧两个时刻。适合的启动状态矩阵场景本地值同步初始化异步读取预期首次安装不存在成功返回默认值首帧和最终都为auto已保存主题背景rain成功返回rain首帧直接显示rain同步读取失败未知抛错异步成功应用不崩溃最终恢复同步成功、异步失败night成功默认值/失败不应把首帧正确值错误覆盖冷启动后切暗色已有主题成功成功背景、文字和系统栏一致更新由于catch (_e) {}会吞掉同步初始化错误设备测试必须观察页面结果和 hilog而不是等待异常直接暴露。建议给DataStore增加可注入接口后在单元层模拟同步失败与异步失败避免测试依赖真实 Preferences 故障。四、Index 很薄测试重点在导航容器契约Index.ets只负责装配 NavigationEntry Component struct Index { StorageLink(StateKeys.NAV_STACK) pathStack: NavPathStack new NavPathStack(); StorageLink(StateKeys.THEME_BG) themeBg: string #F5F0E8; build() { Navigation(this.pathStack) { MainTabShell() } .navDestination(appRouter) .hideTitleBar(true) .hideToolBar(true) .mode(NavigationMode.Stack) .backgroundColor(this.themeBg); } }“薄”不代表可以跳过测试。这里拥有三个发布级契约NAV_STACK必须在AppStore.bootstrap()后可用。appRouter必须能解析所有业务路由。系统返回和页面可见返回按钮必须把栈恢复到正确位置。建议的 UI 自动化不需要覆盖每个页面所有控件而是建立路由烟囱冷启动 - 首页 首页 - 时光相册 - 返回 - 首页 首页 - 心情日记 - 新建 - 取消 - 日记列表 我的 - 隐私政策 - 系统返回 - 我的 全部 - 详情 - 编辑 - 保存 - 返回列表每一步记录当前页面的稳定标识、导航栈深度或可见标题。失败判据包括白屏、返回到错误 Tab、重复返回退出应用、路由参数为空导致异常以及主题背景在目标页恢复为默认色。五、服务卡片的空数据路径已经存在应该固定成契约CountdownFormAbility.getFormData()在仓库有数据时选择指定纪念日否则使用第一条const selectedId DataStore.getInstance() .getJsonSyncstring( DataKeys.WIDGET_ANNIVERSARY_ID, ); const selected items.find( (item: Anniversary) item.id selectedId ); const top selected ?? items[0];如果仓库为空或者读取过程中抛错则返回{ title: 时光清单, days: 0, subtitle: 添加事项开始倒计时, updateTime: Date.now().toString() }这不是临时占位而应成为稳定契约。至少覆盖完全空仓库。已保存的组件 ID 为空。保存的 ID 指向已删除事项。仓库只有一条事项。仓库多条事项且指定 ID 有效。Preferences 内容损坏导致仓库读取异常。targetDate为过去、今天、未来和异常值。其中“已删除选择项”当前会回退到items[0]不会显示空卡片。测试需要固定这个产品决定如果未来希望提示“请重新选择”应先改变契约再改用例而不是悄悄改变回退行为。页面空数据还需要区分“业务为空”和“加载失败”。当前FilteredListView有“暂无某类事项”的明确空态WidgetView也有三种无数据预览和“暂无事项”提示AllView没有独立 empty/error 分支HomeView虽声明isLoading但build()没有用它切换 loading/content/error。回归用例应分别覆盖首次安装、筛选结果为空、Preferences 返回空数组、读取异常四种场景不能让空白列表同时承担加载中、无数据和失败三种含义。六、三种卡片尺寸要测默认值也要测极端文本项目有2x2、2x4和4x4三种 ArkTS 卡片页面。它们都为title与days提供默认值但文本约束并不完全相同LocalStorageProp(title) title: string 倒计时; LocalStorageProp(days) days: string 0;2x2的标题设置了单行省略2x4的标题与副标题限制为单行4x4的副标题限制两行但标题没有maxLines。因此视觉回归不能只截图正常的“四级考试还有 20 天”还应准备输入目的空标题检查默认值是否真的进入卡片40 个中文字符检查标题是否挤压天数区域混合英文长单词检查不可自然断词的文本days -1检查负数和业务语义days 999999检查大数字宽度空 subtitle检查空行是否仍占高度两行超长 subtitle检查4x4是否截断系统深色模式检查硬编码白底与固定文字色form_config.json设置了colorMode: auto但三个卡片页面仍大量使用#FFFFFF、#1A1A1A、#666666。这意味着“自动颜色模式”与“页面实际自适应”不能画等号。卡片测试应在亮色和暗色环境分别截图而不是根据配置字段直接判定适配完成。七、异常输入空标题只是第一层AddView.handleSave()当前对空标题直接返回if (this.title.trim().length 0) return;日期则这样解析const dateMs this.targetDate ? new Date(this.targetDate).getTime() : Date.now() 86400000 * 30;这里有两个不同风险空标题没有错误提示用户只会感觉按钮无响应。非空但非法日期可能得到NaN后续仍构造AnniversarySaveData并保存。回归输入表应该覆盖标题日期预期空字符串空不保存显示标题必填提示全空格合法日期trim 后为空不保存正常标题空使用明确的默认日期策略正常标题2025-13-40不保存显示日期错误正常标题hello不保存显示日期错误100 字标题合法日期按产品长度限制处理emoji 与中英文合法日期保存后列表和卡片不乱码建议把解析提取成纯函数先写单元测试export interface DateParseResult { ok: boolean; value?: number; message?: string; } export function parseTargetDate( raw: string, now: number ): DateParseResult { const value raw.trim(); if (value.length 0) { return { ok: true, value: now 30 * 86400000 }; } const timestamp new Date(value).getTime(); if (!Number.isFinite(timestamp)) { return { ok: false, message: 日期格式不正确 }; } return { ok: true, value: timestamp }; }该函数的职责仅是把输入变成明确结果不操作 UI、不写 Preferences。页面拿到失败结果后展示提示仓库只接收有限数字。测试因此能在毫秒级覆盖大量边界而不必每次启动设备。八、重复点击创建操作必须互斥打卡操作必须幂等保存按钮当前直接等待异步方法Button(保存) .onClick(async () { await this.handleSave(); })handleSave()没有isSaving守卫。快速连点时两个调用可能在第一次清空输入前同时读取相同标题并进入仓库形成两条业务重复记录。createAnniversary()的 ID 使用时间戳加随机后缀降低了 ID 冲突概率却不会阻止重复业务数据。建议引入明确互斥状态State private isSaving: boolean false; private async handleSave(): Promisevoid { if (this.isSaving) return; this.isSaving true; try { const draft this.validateDraft(); if (!draft) return; await this.viewModel.saveAnniversary(draft); this.resetEditor(); } finally { this.isSaving false; } }按钮同时绑定禁用和文案Button(this.isSaving ? 保存中... : 保存) .enabled(!this.isSaving) .onClick(async () { await this.handleSave(); })重复点击用例应模拟在持久化延迟期间触发两次点击并断言saveAnniversary()只调用一次。仓库只增加一条记录。DATA_VERSION只递增一次。成功提示只出现一次。无论成功或失败isSaving最终恢复为false。打卡则是另一种语义。HabitService.checkIn()已通过todayDone让顺序重复调用不再增加次数if (habit.id ! id || habit.todayDone) { return habit; }这类操作适合验证幂等连续调用两次后total和streak只能增加一次。创建纪念日不是天然幂等操作因此更适合 UI 互斥或业务请求键。九、数据刷新也是回归对象项目错误记录中曾出现“详情保存后列表仍显示旧数据”。当前修复通过DATA_VERSION通知其他 Tabconst ver AppStorage.getnumber(StateKeys.DATA_VERSION) ?? 0; AppStorage.setnumber( StateKeys.DATA_VERSION, ver 1 );这条历史问题应该沉淀为永久回归而不是修完后删除测试。建议至少覆盖新建事项后首页和全部列表出现新项。详情页编辑标题后返回列表立即显示新标题。删除后首页、全部页和分类页均消失。置顶后排序变化。首次组件选择写入后卡片读取到同一 ID。编辑仅改变updatedAt时ForEach 行节点重新渲染。测试观察点不能只看 Preferences。即使数据已经落盘UI 没有刷新仍然是用户可见缺陷。反过来只看页面变了也不够重启后恢复旧值说明持久化链路仍然失败。每个修改类用例都应执行“当前页观察 跨页观察 重启恢复”三段验证。十、BackupService 要用坏文件攻击边界BackupService.importBackup()读取整个文件并解析 JSONconst stat fileIo.statSync(filePath); const buf new ArrayBuffer(stat.size); fileIo.readSync(file.fd, buf); const data JSON.parse(json) as BackupData; if (!data.version || !data.anniversaries) { throw new Error(Invalid backup format); }当前校验只要求version和anniversaries为真值。以下内容可能穿过校验{ version: 1, anniversaries: not-an-array }也没有文件大小上限、字段类型、版本范围、时间戳、主题 ID 或数组元素结构验证。回归测试应准备一组固定夹具文件预期合法 v1 备份成功解析空文件抛出可识别错误非 JSON解析失败缺少 version拒绝anniversaries 为字符串拒绝未知 version拒绝或进入迁移超大文件在分配内存前拒绝条目缺少 id/title拒绝或规范化文件不存在错误可观察文件句柄不泄露适合把验证独立为纯函数export function isBackupData( value: Object ): value is BackupData { const candidate value as PartialBackupData; return candidate.version 1 typeof candidate.timestamp number Array.isArray(candidate.anniversaries) Array.isArray(candidate.quotes) typeof candidate.themeId string; }实际生产实现还需要逐项验证数组元素这里的示例先展示边界方向。测试要验证失败时文件正确关闭、原有数据不被覆盖、页面不给出“恢复成功”。十一、Repository 测试需要可替换存储当前还有一个需要专门故障注入的边界DataStore.putJson()与remove()捕获 Preferences 异常后只写日志并返回调用方拿不到失败结果AnniversaryRepository.save()又会先修改内存数组再等待持久化。于是“当前页看见新数据”和“磁盘已经可靠保存”不是同一件事。测试必须注入put或flush失败分别断言内存状态、页面成功提示、DATA_VERSION和重启回读避免把短暂 UI 变化当成持久化成功。AnniversaryRepository当前直接持有单例DataStoreprivate store: DataStore DataStore.getInstance();这使单元测试很难注入“写入延迟”“flush 失败”“返回损坏 JSON”等情况。更可测的契约是定义窄接口export interface JsonStore { putJsonT( key: string, value: T ): Promisevoid; getJsonT( key: string, fallback: T ): PromiseT; }测试用内存实现export class MemoryJsonStore implements JsonStore { private values: Mapstring, Object new Mapstring, Object(); async putJsonT( key: string, value: T ): Promisevoid { this.values.set(key, value as Object); } async getJsonT( key: string, fallback: T ): PromiseT { return (this.values.get(key) as T) ?? fallback; } }这不是为了引入复杂测试框架而是把 HarmonyOS Context 和业务规则分开。纯仓库用例可以快速验证新增、编辑、删除、排序和重复保存设备测试只负责证明 Preferences 适配器在真实系统上工作。十二、Hypium 用例应围绕业务命名把默认assertContain改成可读的业务用例后测试报告才能定位问题import { describe, it, expect } from ohos/hypium; import { calcDaysRemaining } from ../main/ets/model/Anniversary; export default function anniversaryTests() { describe(AnniversaryDateContract, () { it(today_returns_zero_days, 0, () { const today new Date(); today.setHours(12, 0, 0, 0); expect( calcDaysRemaining(today.getTime()) ).assertEqual(0); }); it(invalid_date_must_not_enter_repository, 0, () { const value new Date(invalid).getTime(); expect( Number.isFinite(value) ).assertFalse(); }); }); }第二条示例只暴露风险不等于已经阻止非法日期。真正完成闭环后用例应调用parseTargetDate()并断言ok false。用例名称要写行为不写“test1”“assertEqual”这样失败日志本身就是缺陷说明。推荐套件结构entry/src/test/ List.test.ets AnniversaryDate.test.ets AnniversaryRepository.test.ets HabitIdempotency.test.ets BackupValidator.test.ets entry/src/ohosTest/ets/test/ List.test.ets LaunchAbility.test.ets NavigationFlow.test.ets FormFallback.test.ets业务套件按所有权组织比按“单元/接口/页面”混成一个大文件更容易维护。十三、设备冒烟必须使用发布候选包本地单元测试通过后还不能替代 AppGallery Guideline 3.1 所关心的运行稳定性。发布候选包至少执行一次安装 - 首次启动 - 首页空状态 - 新建纪念日 - 查看详情 - 编辑 - 返回 - 选择桌面卡片事项 - 添加三种尺寸卡片 - 切换亮暗色 - 进入隐私政策 - 返回 - 前后台切换 - 强制结束 - 冷启动恢复 - 删除事项 - 卡片更新或回退 - 卸载记录项包括安装包身份、版本、签名类型和构建时间。设备型号、HarmonyOS 版本、窗口形态。启动是否白屏、闪退、卡死或无响应。hilog 中第一条真实错误。三种卡片空数据与长文本截图。重复点击前后仓库条目数量。卸载是否正常完成。不要用 DevEco 预览截图替代发布包安装结果也不要把 debug 包通过描述成 release 包通过。无法连接真机时应把设备冒烟明确标为“未运行”而不是默认成功。十四、每次修复最多建立一个清晰证据链回归失败后最容易走向两个极端只修页面表现不验证存储或者一次重构整个架构无法判断哪个修改真正解决了问题。更稳的循环是用最小输入稳定复现例如连续双击保存。记录失败状态例如仓库新增两条、版本号递增两次。在拥有该行为的最窄层修复例如页面保存互斥。重跑同一个用例确认风险收敛。再跑受影响的跨页和重启回归。最后运行构建和发布包冒烟。对于本项目已有的历史问题测试名称可以直接对应现象editing_item_refreshes_all_lists_immediately mood_background_survives_cold_start starry_theme_keeps_tab_text_readable deleted_widget_item_falls_back_safely double_tap_save_creates_one_item这样测试不仅保护代码也保护已付出的排障成本。十五、发布前回归清单启动与生命周期[ ] 首次安装无数据可启动不白屏、不闪退。[ ] 有历史 Preferences 数据时首帧不闪回默认值。[ ] 同步初始化失败后异步恢复路径可用。[ ] 前后台切换和系统深浅色变化后页面状态一致。[ ] Navigation 返回栈不重复、不丢失 Tab。数据与输入[ ] 空标题、全空格、非法日期均不进入仓库。[ ] 超长标题、emoji 和中英文混排可保存与展示。[ ] 新增、编辑、删除、置顶后跨页立即刷新。[ ] 重启后数据与界面一致。[ ] 保存失败不显示成功状态。重复操作[ ] 连续双击保存只产生一条数据。[ ] 连续打卡只增加一次统计。[ ] 重复删除不会误删其他 ID。[ ] 快速切 Tab 不创建重复页面状态。[ ] 重复触发卡片更新不崩溃。卡片与备份[ ]2x2、2x4、4x4均覆盖空数据。[ ] 已选事项删除后回退行为符合产品约定。[ ] 长标题、大数字、暗色模式截图无截断和低对比。[ ] 损坏、缺字段、超大和未知版本备份被拒绝。[ ] 导入失败不覆盖原有数据。发布包[ ] 相关单元测试和设备测试实际运行。[ ]assembleHap或项目约定构建命令通过。[ ] release 候选包完成安装、启动、核心流和卸载。[ ] AppAnalyzer 或上架预检结果已复核。[ ] 未运行的设备、场景和剩余风险被明确记录。十六、常见失败与定位顺序现象可能原因第一检查点冷启动背景先错后对只异步 hydrateEntryAbility.onCreate()顺序首次安装卡片空白空仓库无默认 FormDatagetFormData()兜底卡片显示其他事项已选 ID 删除后回退第一条选择 ID 与回退契约保存按钮无反应空标题静默 return输入校验与错误状态列表出现两条相同事项保存中未禁用按钮isSaving与调用次数无效日期导致排序异常NaN已进入仓库日期解析结果打卡次数偶发异常重复点击或并发读写todayDone与持久化时序恢复后应用崩溃备份只做浅层校验版本与字段验证测试全绿但功能仍坏仍在运行模板断言测试套件业务覆盖debug 正常、上架包失败未测 release 候选包签名、安装、启动证据定位时先看第一个可复现的业务状态再看 hilog 和持久化最后才扩大到系统环境。测试不是把日志变长而是把“偶尔不对”变成一个可以重复执行的失败条件。十七、总结把历史缺陷变成永久发布门禁时光清单已经具备测试目录、启动分层初始化、Navigation 容器、三种桌面卡片和本地数据服务但现有 Hypium 文件仍停留在模板断言无法支撑业务稳定性结论。最值得优先补齐的不是页面点击数量而是五条高风险路径首帧启动、空数据、非法输入、重复操作和损坏恢复。其中卡片空数据已经有明确默认值可以直接固化成契约打卡已有顺序幂等判断应补连续调用用例日期解析和备份校验存在可复核缺口应先提取纯函数创建操作没有保存互斥应通过延迟存储模拟连续点击。最后再用发布候选包完成安装、启动、核心流、前后台、卡片和卸载冒烟。一套好的回归测试不承诺“永远没有问题”它保证每个修过的问题都留下可重复证据每个新增能力都知道自己会影响哪条发布门禁。这样当前 HarmonyOS 工程的稳定性不再依赖最后一天手工点一遍而成为持续可执行的工程资产。**AI 辅助声明**本文部分内容由 AI 辅助整理和配图。测试目录、启动顺序、空态、输入校验、重复点击风险、数据刷新、卡片回退、备份校验与持久化边界均依据“时光清单”当前真实源码复核建议代码和验收矩阵未写成已实现结果也未虚构测试通过数、构建结果、设备数据、发布数据、用户数据或平台评分。
