【时光清单|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击
**本轮证据边界:**本文基于
D:\huawei\one8(时光清单)当前源码、测试目录与项目错误记录做静态复核。当前仓库可见 6 个模板it()声明,但本轮没有执行测试、构建、安装、模拟器、真机、故障注入或远端发布;源码声明绝不等于实际通过。阅读约定:"当前事实"来自文中列出的真实文件;"历史证据"仅指既有项目记录;日期解析器、保存互斥、可替换存储、业务测试套件和设备矩阵均为建议实现,不代表当前工程已经落地。
一个 HarmonyOS 应用能够编译,并不代表它已经具备可上架的稳定性。真正容易在回归阶段暴露的问题,往往不是某个 API 不会调用,而是状态组合没有被测试:首次安装没有任何数据时卡片显示什么,Preferences 初始化失败后首页会不会白屏,日期输入为无效字符串时是否把 NaN 写入仓库,用户连续点击两次"保存"会不会创建两条纪念日,备份文件只有 version 和空数组时能否被错误地接受。
时光清单 的真实源码已经包含 entry/src/test 与 entry/src/ohosTest,但其中用例仍是工程模板自带的 assertContain。这只能证明源码中存在测试入口和一个简单断言,不能证明启动链路、Navigation、服务卡片、持久化或用户输入经受过回归。与此同时,业务代码里已经出现了很适合转成测试契约的边界:EntryAbility 同步初始化后再异步 hydrate,CountdownFormAbility 在空数据和异常时返回默认卡片,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()
.getJsonSync<string>(
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 只负责装配 Navigation:
@Entry
@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()
.getJsonSync<string>(
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(): Promise<void> {
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 通知其他 Tab:
const ver =
AppStorage.get<number>(StateKeys.DATA_VERSION) ?? 0;
AppStorage.set<number>(
StateKeys.DATA_VERSION,
ver + 1
);
这条历史问题应该沉淀为永久回归,而不是修完后删除测试。建议至少覆盖:
- 新建事项后首页和全部列表出现新项。
- 详情页编辑标题后,返回列表立即显示新标题。
- 删除后首页、全部页和分类页均消失。
- 置顶后排序变化。
- 首次组件选择写入后,卡片读取到同一 ID。
- 编辑仅改变
updatedAt时,ForEach 行节点重新渲染。
测试观察点不能只看 Preferences。即使数据已经落盘,UI 没有刷新仍然是用户可见缺陷。反过来,只看页面变了也不够:重启后恢复旧值说明持久化链路仍然失败。每个修改类用例都应执行"当前页观察 + 跨页观察 + 重启恢复"三段验证。
十、BackupService 要用坏文件攻击边界
BackupService.importBackup() 读取整个文件并解析 JSON:
const 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 Partial<BackupData>;
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 当前直接持有单例 DataStore:
private store: DataStore =
DataStore.getInstance();
这使单元测试很难注入"写入延迟""flush 失败""返回损坏 JSON"等情况。更可测的契约是定义窄接口:
export interface JsonStore {
putJson<T>(
key: string,
value: T
): Promise<void>;
getJson<T>(
key: string,
fallback: T
): Promise<T>;
}
测试用内存实现:
export class MemoryJsonStore
implements JsonStore {
private values: Map<string, Object> =
new Map<string, Object>();
async putJson<T>(
key: string,
value: T
): Promise<void> {
this.values.set(key, value as Object);
}
async getJson<T>(
key: string,
fallback: T
): Promise<T> {
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 或上架预检结果已复核。
- 未运行的设备、场景和剩余风险被明确记录。
十六、常见失败与定位顺序
| 现象 | 可能原因 | 第一检查点 |
|---|---|---|
| 冷启动背景先错后对 | 只异步 hydrate | EntryAbility.onCreate() 顺序 |
| 首次安装卡片空白 | 空仓库无默认 FormData | getFormData() 兜底 |
| 卡片显示其他事项 | 已选 ID 删除后回退第一条 | 选择 ID 与回退契约 |
| 保存按钮无反应 | 空标题静默 return | 输入校验与错误状态 |
| 列表出现两条相同事项 | 保存中未禁用按钮 | isSaving 与调用次数 |
| 无效日期导致排序异常 | NaN 已进入仓库 |
日期解析结果 |
| 打卡次数偶发异常 | 重复点击或并发读写 | todayDone 与持久化时序 |
| 恢复后应用崩溃 | 备份只做浅层校验 | 版本与字段验证 |
| 测试全绿但功能仍坏 | 仍在运行模板断言 | 测试套件业务覆盖 |
| debug 正常、上架包失败 | 未测 release 候选包 | 签名、安装、启动证据 |
定位时先看第一个可复现的业务状态,再看 hilog 和持久化,最后才扩大到系统环境。测试不是把日志变长,而是把"偶尔不对"变成一个可以重复执行的失败条件。
十七、总结:把历史缺陷变成永久发布门禁
时光清单 已经具备测试目录、启动分层初始化、Navigation 容器、三种桌面卡片和本地数据服务,但现有 Hypium 文件仍停留在模板断言,无法支撑业务稳定性结论。最值得优先补齐的不是页面点击数量,而是五条高风险路径:首帧启动、空数据、非法输入、重复操作和损坏恢复。
其中,卡片空数据已经有明确默认值,可以直接固化成契约;打卡已有顺序幂等判断,应补连续调用用例;日期解析和备份校验存在可复核缺口,应先提取纯函数;创建操作没有保存互斥,应通过延迟存储模拟连续点击。最后,再用发布候选包完成安装、启动、核心流、前后台、卡片和卸载冒烟。
一套好的回归测试不承诺"永远没有问题",它保证每个修过的问题都留下可重复证据,每个新增能力都知道自己会影响哪条发布门禁。这样,当前 HarmonyOS 工程的稳定性不再依赖最后一天手工点一遍,而成为持续可执行的工程资产。
**AI 辅助声明:**本文部分内容由 AI 辅助整理和配图。测试目录、启动顺序、空态、输入校验、重复点击风险、数据刷新、卡片回退、备份校验与持久化边界均依据"时光清单"当前真实源码复核;建议代码和验收矩阵未写成已实现结果,也未虚构测试通过数、构建结果、设备数据、发布数据、用户数据或平台评分。