渐进式编程课
鸿蒙 目录
第 39 课2026.08.25

适用场景:保存、校验、上传等步骤必须串行执行,且任一中间步骤失败后都要立即中止后续动作并向上传递错误

第 39 课:Hypium 多阶段中间失败测试

  • 日期:2026-08-25
  • 课程序号:第 39 课
  • 知识点:精确触发串行流程的中间阶段失败,验证后续阶段不启动
  • 适用场景:保存、校验、上传等步骤必须串行执行,且任一中间步骤失败后都要立即中止后续动作并向上传递错误

承接上一课:上一课逐个释放两个成功步骤,证明串行流程不会跳步;本课让第二阶段可控地失败,验证第三阶段不会启动。

核心概念

  • 中间步骤同时提供 started 和可控的 reject,测试可以先确认它已启动,再精确触发失败。
  • await 收到拒绝后,被测方法直接结束;位于该等待点之后的上传和成功事件都不应发生。
  • 失败测试要同时断言错误内容和后续阶段未启动,才能覆盖“错误传播”与“副作用中止”两个契约。
  • 使用明确的阶段信号,不用 setTimeout 猜测异步流程运行到了哪里。

最小 ArkTS / Hypium 示例

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

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

class ControlledRejectStep {
  private markStarted: () => void = (): void => {};
  private rejectPending: (error: Error) => void = (_error: Error): void => {};
  readonly started: Promise<void> = new Promise<void>((resolve: () => void) => {
    this.markStarted = resolve;
  });
  private readonly pending: Promise<void> = new Promise<void>((_resolve: () => void,
    reject: (error: Error) => void) => {
    this.rejectPending = reject;
  });

  async run(): Promise<void> {
    this.markStarted();
    await this.pending;
  }

  reject(error: Error): void {
    this.rejectPending(error);
  }
}

class UploadSpy {
  called: boolean = false;

  async run(): Promise<void> {
    this.called = true;
  }
}

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

  constructor(private readonly validateStep: ControlledRejectStep,
    private readonly uploadStep: UploadSpy) {}

  async publish(): Promise<void> {
    this.events.push('save-finished');
    await this.validateStep.run();
    this.events.push('validate-finished');
    await this.uploadStep.run();
    this.events.push('publish-success');
  }
}

export default function middleStageRejectTest(): void {
  describe('PublishController.publish rejects during validation', () => {
    it('doesNotStartUploadAfterValidationFails', 0, async () => {
      // Arrange
      const validateStep: ControlledRejectStep = new ControlledRejectStep();
      const uploadStep: UploadSpy = new UploadSpy();
      const controller: PublishController = new PublishController(validateStep, uploadStep);

      // Act:等待校验阶段确实启动,再精确触发失败
      const publishing: Promise<void> = controller.publish();
      await validateStep.started;
      validateStep.reject(new Error('校验失败'));
      const result: PromiseResult = await publishing.then(
        (): PromiseResult => ({ rejected: false, message: '' }),
        (error: Error): PromiseResult => ({ rejected: true, message: error.message })
      );

      // Assert:错误向上传递,上传和成功事件均未发生
      expect(result.rejected).assertTrue();
      expect(result.message).assertEqual('校验失败');
      expect(uploadStep.called).assertFalse();
      expect(controller.events.join(',')).assertEqual('save-finished');
    });
  });
}

这里先等待 validateStep.started,因此失败发生在确定的第二阶段;uploadStep.called 为 false 则证明流程没有越过失败的 await。

3~5 分钟练习

给 PublishController 增加 failedCount,要求校验失败时加一后继续把原错误抛给调用方;补充对应断言。

参考答案

ts
async publish(): Promise<void> {
  this.events.push('save-finished');
  try {
    await this.validateStep.run();
  } catch (error) {
    this.failedCount += 1;
    throw error;
  }
  this.events.push('validate-finished');
  await this.uploadStep.run();
  this.events.push('publish-success');
}

expect(controller.failedCount).assertEqual(1);
expect(result.message).assertEqual('校验失败');
expect(uploadStep.called).assertFalse();

捕获错误只用于记录失败次数,随后立即 throw error,因此不会把失败伪装成成功,也不会启动上传。

与上一课的联系

上一课用多个可控 Promise 验证全部成功时的阶段顺序;本课保持同一套阶段信号,把第二阶段改为可控拒绝,补齐多阶段流程的中间失败契约。