渐进式编程课
鸿蒙 目录
第 40 课2026.08.26

适用场景:保存、校验、上传等多阶段流程需要系统验证“任一步失败后,只停止其后的步骤”

第 40 课:Hypium 多阶段失败矩阵

  • 日期:2026-08-26
  • 课程序号:第 40 课
  • 知识点:用参数化用例覆盖串行流程每个阶段的失败与中止边界
  • 适用场景:保存、校验、上传等多阶段流程需要系统验证“任一步失败后,只停止其后的步骤”

承接上一课:上一课只验证第二阶段失败;本课把失败位置整理成数据表,用独立用例覆盖第一、第二、第三阶段。

核心概念

  • 每个用例只指定 failAt、期望错误和期望事件,避免复制三套测试流程。
  • 事件序列既证明失败发生在哪一阶段,也证明失败点之后的步骤没有启动。
  • 为每组数据注册独立的 it,失败报告会直接显示具体阶段,而不是只告诉你整个循环失败。
  • 参数化减少重复代码,但不能弱化断言:错误传播和副作用中止仍要同时验证。

最小 ArkTS / Hypium 示例

ts
import { describe, expect, it } from '@ohos/hypium';

interface FailureCase {
  name: string;
  failAt: string;
  expectedMessage: string;
  expectedEvents: string;
}

interface PromiseResult {
  rejected: boolean;
  message: string;
}

class Stage {
  constructor(private readonly name: string,
    private readonly failAt: string,
    private readonly events: string[]) {}

  async run(): Promise<void> {
    this.events.push(`${this.name}-started`);
    if (this.failAt === this.name) {
      throw new Error(`${this.name}失败`);
    }
    this.events.push(`${this.name}-finished`);
  }
}

class PublishController {
  readonly events: string[] = [];

  async publish(failAt: string): Promise<void> {
    await new Stage('保存', failAt, this.events).run();
    await new Stage('校验', failAt, this.events).run();
    await new Stage('上传', failAt, this.events).run();
    this.events.push('发布成功');
  }
}

const cases: FailureCase[] = [
  {
    name: '保存失败后不启动校验',
    failAt: '保存',
    expectedMessage: '保存失败',
    expectedEvents: '保存-started'
  },
  {
    name: '校验失败后不启动上传',
    failAt: '校验',
    expectedMessage: '校验失败',
    expectedEvents: '保存-started,保存-finished,校验-started'
  },
  {
    name: '上传失败后不报告成功',
    failAt: '上传',
    expectedMessage: '上传失败',
    expectedEvents: '保存-started,保存-finished,校验-started,校验-finished,上传-started'
  }
];

export default function multiStageFailureMatrixTest(): void {
  describe('PublishController.publish failure matrix', () => {
    cases.forEach((testCase: FailureCase) => {
      it(testCase.name, 0, async () => {
        // Arrange
        const controller: PublishController = new PublishController();

        // Act
        const result: PromiseResult = await controller.publish(testCase.failAt).then(
          (): PromiseResult => ({ rejected: false, message: '' }),
          (error: Error): PromiseResult => ({ rejected: true, message: error.message })
        );

        // Assert
        expect(result.rejected).assertTrue();
        expect(result.message).assertEqual(testCase.expectedMessage);
        expect(controller.events.join(',')).assertEqual(testCase.expectedEvents);
      });
    });
  });
}

事件中同时记录 started 和 finished:例如上传失败时应出现 上传-started,但不能出现 上传-finished 或 发布成功。

3~5 分钟练习

在失败矩阵中增加“无失败”的成功用例,要求三个阶段全部完成,并最终记录 发布成功。

参考答案

ts
interface PublishCase {
  name: string;
  failAt: string;
  rejected: boolean;
  expectedEvents: string;
}

const successCase: PublishCase = {
  name: '全部阶段成功后报告发布成功',
  failAt: '',
  rejected: false,
  expectedEvents: '保存-started,保存-finished,校验-started,校验-finished,' +
    '上传-started,上传-finished,发布成功'
};

const result: PromiseResult = await controller.publish(successCase.failAt).then(
  (): PromiseResult => ({ rejected: false, message: '' }),
  (error: Error): PromiseResult => ({ rejected: true, message: error.message })
);
expect(result.rejected).assertFalse();
expect(controller.events.join(',')).assertEqual(successCase.expectedEvents);

成功用例与失败矩阵共用同一事件协议,能同时守住“失败立即中止”和“全部完成才成功”两侧边界。

与上一课的联系

上一课用可控拒绝精确验证第二阶段失败;本课把相同的错误传播与后续中止契约扩展成失败位置矩阵,系统覆盖串行流程的每一个阶段。