渐进式编程课
鸿蒙 目录
第 29 课2026.08.12

适用场景:同一个异步业务方法既要验证成功后的交互,也要验证失败后的中断行为

第 29 课:Hypium 成功与失败契约测试

  • 日期:2026-08-12
  • 课程序号:第 29 课
  • 知识点:用 beforeEach 为同一业务对象组织成功与失败契约测试
  • 适用场景:同一个异步业务方法既要验证成功后的交互,也要验证失败后的中断行为

承接上一课:上一课单独验证了保存失败时不发送通知;本课把成功与失败用例放进同一测试套件,并用 beforeEach 消除重复准备代码。

核心概念

  • beforeEach 会在每个 it 前重新创建依赖和被测对象,避免调用次数、失败配置在用例之间残留。
  • 可配置 Fake 用 shouldFail 控制保存成功或失败;Spy 只记录通知次数。
  • 成功用例验证“保存后通知”,失败用例验证“异常上传且不通知”,两者共同描述完整业务契约。

最小 ArkTS / Hypium 示例

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

interface DraftService {
  save(content: string): Promise<void>;
}

interface NoticeService {
  send(message: string): void;
}

class DraftPublisher {
  constructor(
    private draftService: DraftService,
    private noticeService: NoticeService
  ) {}

  async publish(content: string): Promise<void> {
    await this.draftService.save(content);
    this.noticeService.send('发布成功');
  }
}

class ConfigurableDraftService implements DraftService {
  shouldFail: boolean = false;

  save(content: string): Promise<void> {
    if (this.shouldFail) {
      return Promise.reject(new Error(`保存失败:${content}`));
    }
    return Promise.resolve();
  }
}

class SpyNoticeService implements NoticeService {
  callCount: number = 0;

  send(message: string): void {
    this.callCount += 1;
  }
}

export default function draftPublisherContractTest(): void {
  describe('DraftPublisher contract', () => {
    let draftService: ConfigurableDraftService = new ConfigurableDraftService();
    let noticeService: SpyNoticeService = new SpyNoticeService();
    let publisher: DraftPublisher = new DraftPublisher(draftService, noticeService);

    beforeEach(() => {
      draftService = new ConfigurableDraftService();
      noticeService = new SpyNoticeService();
      publisher = new DraftPublisher(draftService, noticeService);
    });

    it('notifiesWhenSaveSucceeds', 0, async () => {
      await publisher.publish('鸿蒙笔记');

      expect(noticeService.callCount).assertEqual(1);
    });

    it('doesNotNotifyWhenSaveFails', 0, async () => {
      draftService.shouldFail = true;
      let errorMessage: string = '';

      try {
        await publisher.publish('鸿蒙笔记');
      } catch (error) {
        errorMessage = (error as Error).message;
      }

      expect(errorMessage).assertEqual('保存失败:鸿蒙笔记');
      expect(noticeService.callCount).assertEqual(0);
    });
  });
}

每个用例都拿到全新的 Fake、Spy 和 DraftPublisher;因此成功用例产生的 callCount = 1 不会污染失败用例。

3~5 分钟练习

删除 beforeEach 中重新创建 noticeService 的语句,并让失败用例紧跟成功用例运行。观察失败用例的调用次数断言为什么可能失败,然后恢复该语句。

参考答案

若复用同一个 noticeService,成功用例已经把 callCount 改为 1;失败用例即使没有发送新通知,也会读到旧值。每个用例前重新创建 Spy,可保证测试互不影响:

ts
beforeEach(() => {
  draftService = new ConfigurableDraftService();
  noticeService = new SpyNoticeService();
  publisher = new DraftPublisher(draftService, noticeService);
});

与上一课的联系

上一课证明了单独的失败路径;本课把它与成功路径合并为同一套契约测试,并通过 beforeEach 隔离两条路径的测试状态。