【时光清单|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击

【时光清单|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击

**本轮证据边界:**本文基于 D:\huawei\one8(时光清单) 当前源码、测试目录与项目错误记录做静态复核。当前仓库可见 6 个模板 it() 声明,但本轮没有执行测试、构建、安装、模拟器、真机、故障注入或远端发布;源码声明绝不等于实际通过。

阅读约定:"当前事实"来自文中列出的真实文件;"历史证据"仅指既有项目记录;日期解析器、保存互斥、可替换存储、业务测试套件和设备矩阵均为建议实现,不代表当前工程已经落地。

一个 HarmonyOS 应用能够编译,并不代表它已经具备可上架的稳定性。真正容易在回归阶段暴露的问题,往往不是某个 API 不会调用,而是状态组合没有被测试:首次安装没有任何数据时卡片显示什么,Preferences 初始化失败后首页会不会白屏,日期输入为无效字符串时是否把 NaN 写入仓库,用户连续点击两次"保存"会不会创建两条纪念日,备份文件只有 version 和空数组时能否被错误地接受。

时光清单 的真实源码已经包含 entry/src/testentry/src/ohosTest,但其中用例仍是工程模板自带的 assertContain。这只能证明源码中存在测试入口和一个简单断言,不能证明启动链路、Navigation、服务卡片、持久化或用户输入经受过回归。与此同时,业务代码里已经出现了很适合转成测试契约的边界:EntryAbility 同步初始化后再异步 hydrate,CountdownFormAbility 在空数据和异常时返回默认卡片,AddView 对空标题直接返回但没有日期有效性判断,BackupService 只校验两个字段,保存按钮没有处理中互斥状态。

本文面向当前 HarmonyOS 工程,从这些真实代码出发设计一套分层回归策略。重点不是堆用例数量,而是让每个高风险行为都有明确输入、可观察结果、失败判据和清理方法。文中的测试代码是针对当前结构给出的落地建议;除明确引用的现有模板外,不会把建议用例描述成项目已经执行通过。

本文将解决:

  1. 如何判断现有 Hypium 用例是否真的覆盖业务。
  2. 启动同步初始化、异步 hydrate 与首帧状态怎样测试。
  3. 空仓库、已删除选择项和异常数据下,桌面卡片应如何兜底。
  4. 空标题、非法日期、损坏备份和超长文本怎样构造。
  5. 重复点击如何验证幂等、互斥与数据版本通知。
  6. 单元测试、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);
  }
}

"薄"不代表可以跳过测试。这里拥有三个发布级契约:

  1. NAV_STACK 必须在 AppStore.bootstrap() 后可用。
  2. appRouter 必须能解析所有业务路由。
  3. 系统返回和页面可见返回按钮必须把栈恢复到正确位置。

建议的 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 返回空数组、读取异常四种场景,不能让空白列表同时承担加载中、无数据和失败三种含义。

六、三种卡片尺寸要测默认值,也要测极端文本

项目有 2x22x44x4 三种 ArkTS 卡片页面。它们都为 titledays 提供默认值,但文本约束并不完全相同:

复制代码
@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;
}

这类操作适合验证幂等:连续调用两次后 totalstreak 只能增加一次。创建纪念日不是天然幂等操作,因此更适合 UI 互斥或业务请求键。

九、数据刷新也是回归对象

项目错误记录中曾出现"详情保存后列表仍显示旧数据"。当前修复通过 DATA_VERSION 通知其他 Tab:

复制代码
const ver =
  AppStorage.get<number>(StateKeys.DATA_VERSION) ?? 0;
AppStorage.set<number>(
  StateKeys.DATA_VERSION,
  ver + 1
);

这条历史问题应该沉淀为永久回归,而不是修完后删除测试。建议至少覆盖:

  1. 新建事项后首页和全部列表出现新项。
  2. 详情页编辑标题后,返回列表立即显示新标题。
  3. 删除后首页、全部页和分类页均消失。
  4. 置顶后排序变化。
  5. 首次组件选择写入后,卡片读取到同一 ID。
  6. 编辑仅改变 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');
}

当前校验只要求 versionanniversaries 为真值。以下内容可能穿过校验:

复制代码
{
  "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() 又会先修改内存数组,再等待持久化。于是"当前页看见新数据"和"磁盘已经可靠保存"不是同一件事。测试必须注入 putflush 失败,分别断言内存状态、页面成功提示、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 包通过。无法连接真机时,应把设备冒烟明确标为"未运行",而不是默认成功。

十四、每次修复最多建立一个清晰证据链

回归失败后最容易走向两个极端:只修页面表现,不验证存储;或者一次重构整个架构,无法判断哪个修改真正解决了问题。更稳的循环是:

  1. 用最小输入稳定复现,例如连续双击保存。
  2. 记录失败状态,例如仓库新增两条、版本号递增两次。
  3. 在拥有该行为的最窄层修复,例如页面保存互斥。
  4. 重跑同一个用例,确认风险收敛。
  5. 再跑受影响的跨页和重启回归。
  6. 最后运行构建和发布包冒烟。

对于本项目已有的历史问题,测试名称可以直接对应现象:

复制代码
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 不创建重复页面状态。
  • 重复触发卡片更新不崩溃。

卡片与备份

  • 2x22x44x4 均覆盖空数据。
  • 已选事项删除后回退行为符合产品约定。
  • 长标题、大数字、暗色模式截图无截断和低对比。
  • 损坏、缺字段、超大和未知版本备份被拒绝。
  • 导入失败不覆盖原有数据。

发布包

  • 相关单元测试和设备测试实际运行。
  • assembleHap 或项目约定构建命令通过。
  • release 候选包完成安装、启动、核心流和卸载。
  • AppAnalyzer 或上架预检结果已复核。
  • 未运行的设备、场景和剩余风险被明确记录。

十六、常见失败与定位顺序

现象 可能原因 第一检查点
冷启动背景先错后对 只异步 hydrate EntryAbility.onCreate() 顺序
首次安装卡片空白 空仓库无默认 FormData getFormData() 兜底
卡片显示其他事项 已选 ID 删除后回退第一条 选择 ID 与回退契约
保存按钮无反应 空标题静默 return 输入校验与错误状态
列表出现两条相同事项 保存中未禁用按钮 isSaving 与调用次数
无效日期导致排序异常 NaN 已进入仓库 日期解析结果
打卡次数偶发异常 重复点击或并发读写 todayDone 与持久化时序
恢复后应用崩溃 备份只做浅层校验 版本与字段验证
测试全绿但功能仍坏 仍在运行模板断言 测试套件业务覆盖
debug 正常、上架包失败 未测 release 候选包 签名、安装、启动证据

定位时先看第一个可复现的业务状态,再看 hilog 和持久化,最后才扩大到系统环境。测试不是把日志变长,而是把"偶尔不对"变成一个可以重复执行的失败条件。

十七、总结:把历史缺陷变成永久发布门禁

时光清单 已经具备测试目录、启动分层初始化、Navigation 容器、三种桌面卡片和本地数据服务,但现有 Hypium 文件仍停留在模板断言,无法支撑业务稳定性结论。最值得优先补齐的不是页面点击数量,而是五条高风险路径:首帧启动、空数据、非法输入、重复操作和损坏恢复。

其中,卡片空数据已经有明确默认值,可以直接固化成契约;打卡已有顺序幂等判断,应补连续调用用例;日期解析和备份校验存在可复核缺口,应先提取纯函数;创建操作没有保存互斥,应通过延迟存储模拟连续点击。最后,再用发布候选包完成安装、启动、核心流、前后台、卡片和卸载冒烟。

一套好的回归测试不承诺"永远没有问题",它保证每个修过的问题都留下可重复证据,每个新增能力都知道自己会影响哪条发布门禁。这样,当前 HarmonyOS 工程的稳定性不再依赖最后一天手工点一遍,而成为持续可执行的工程资产。


**AI 辅助声明:**本文部分内容由 AI 辅助整理和配图。测试目录、启动顺序、空态、输入校验、重复点击风险、数据刷新、卡片回退、备份校验与持久化边界均依据"时光清单"当前真实源码复核;建议代码和验收矩阵未写成已实现结果,也未虚构测试通过数、构建结果、设备数据、发布数据、用户数据或平台评分。

相关推荐
坚果的博客2 小时前
Flutter OHOS 环境搭建实战:oh-3.44.9-dev 从 0 到 1 完整记录
flutter·华为·harmonyos
坚果的博客2 小时前
认识 CPF-RN:React Native 鸿蒙官方社区与四大核心仓库
react native·react.js·harmonyos
大雷神3 小时前
HarmonyOS ArkGraphics 2D可变帧率实操——同屏对比30、60和90 FPS动画
华为·harmonyos
OH_TPC3 小时前
【鸿蒙优选三方库】@react-native-ohos/react-native-fast-image:高性能图片加载与缓存组件
缓存·华为·harmonyos·鸿蒙
贾伟康5 小时前
【句匠|03】HarmonyOS ArkTS 每日打卡实战:实现连续天数、补签边界和本地日期判断
harmonyos·arkts·本地存储·每日打卡·学习应用
小雨青年5 小时前
【HarmonyOS 7 平行视界深度实战】02 从普通页面到第一个平行视界 Demo
华为·harmonyos
~远在太平洋~5 小时前
06-鸿蒙系统 uitest UI 自动化指南
ui·自动化·harmonyos
OH_TPC5 小时前
OpenHarmony平台RN三方库适配情况
华为·harmonyos·鸿蒙