渐进式编程课
鸿蒙 目录
第 31 课2026.08.14

适用场景:一个测试同时检查异常、调用次数和多个状态,失败时难以判断是哪条业务契约被破坏

第 31 课:Hypium 单一行为断言原则

  • 日期:2026-08-14
  • 课程序号:第 31 课
  • 知识点:让每个测试用例只验证一个主要行为
  • 适用场景:一个测试同时检查异常、调用次数和多个状态,失败时难以判断是哪条业务契约被破坏

承接上一课:上一课用行为化命名和 AAA 结构让测试更易读;本课进一步让测试名与失败原因保持一一对应。

核心概念

  • 一个 it 应描述一个主要行为,例如“保存失败时异常向上传递”或“保存失败时不发送通知”。
  • 不要机械地要求每个用例只能写一行 expect;多个断言若共同证明同一个行为,可以保留在同一用例中。
  • 当几个断言代表不同契约、任意一个都可能独立失败时,应拆成独立用例,让失败报告直接指出被破坏的行为。

最小 ArkTS / Hypium 示例

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

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

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

class FailingDraftService implements DraftService {
  save(content: string): Promise<void> {
    return Promise.reject(new Error(`保存失败:${content}`));
  }
}

class SpyNoticeService implements NoticeService {
  callCount: number = 0;

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

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

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

export default function draftPublisherFocusedTest(): void {
  describe('DraftPublisher', () => {
    it('propagatesErrorWhenSaveFails', 0, async () => {
      // Arrange
      const publisher: DraftPublisher = new DraftPublisher(
        new FailingDraftService(),
        new SpyNoticeService()
      );
      let errorMessage: string = '';

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

      // Assert:只验证异常上传契约
      expect(errorMessage).assertEqual('保存失败:鸿蒙笔记');
    });

    it('doesNotNotifyWhenSaveFails', 0, async () => {
      // Arrange
      const noticeService: SpyNoticeService = new SpyNoticeService();
      const publisher: DraftPublisher = new DraftPublisher(
        new FailingDraftService(),
        noticeService
      );

      // Act
      try {
        await publisher.publish('鸿蒙笔记');
      } catch (error) {
        // 本用例不验证异常内容,只等待失败流程结束
      }

      // Assert:只验证失败后不通知契约
      expect(noticeService.callCount).assertEqual(0);
    });
  });
}

两个用例使用相同失败条件,但分别验证“异常向上传递”和“不会发送通知”。测试失败时,用例名会直接指出被破坏的契约。

3~5 分钟练习

下面的 testPublishFailure 同时断言错误文案和通知次数。请把它拆成两个行为化命名的用例,并写出各自唯一的主要断言。

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

参考答案

ts
it('propagatesErrorWhenSaveFails', 0, async () => {
  // 执行失败发布并捕获错误
  expect(errorMessage).assertEqual('保存失败:鸿蒙笔记');
});

it('doesNotNotifyWhenSaveFails', 0, async () => {
  // 执行失败发布并检查 Spy
  expect(noticeService.callCount).assertEqual(0);
});

实际代码中,每个用例仍需独立完成 Arrange 和 Act;这里省略重复部分,只突出拆分后的行为名称与断言。

与上一课的联系

上一课让一个用例通过“条件 + 结果”的名称和 AAA 分段变得易读;本课让一个名称只对应一个主要行为,使失败报告进一步做到原因清晰、定位直接。