渐进式编程课
鸿蒙 目录
第 26 课2026.08.07

适用场景:业务结果相同,但仍需确认保存、上报或通知等依赖是否被调用了正确次数

第 26 课:Hypium 交互验证与调用次数

  • 日期:2026-08-07
  • 课程序号:第 26 课
  • 知识点:用 Spy 测试替身记录依赖调用次数,验证关键交互契约
  • 适用场景:业务结果相同,但仍需确认保存、上报或通知等依赖是否被调用了正确次数

承接上一课:上一课用 Fake 服务控制返回值并记录参数;本课让测试替身继续记录调用次数,验证被测对象与依赖之间的交互契约。

核心概念

  • Spy(间谍测试替身):在提供可控行为的同时,记录方法被调用的次数和参数。
  • 调用次数属于交互契约。例如“一次用户操作只能保存一次”值得断言;内部辅助函数调用了几次通常属于实现细节,不应写进测试。
  • 每个测试都创建新的 Spy,避免不同用例共享计数而相互影响。

最小 ArkTS / Hypium 示例

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

interface FavoriteService {
  save(stockCode: string): Promise<void>;
}

class FavoriteManager {
  constructor(private favoriteService: FavoriteService) {}

  async add(stockCode: string): Promise<string> {
    await this.favoriteService.save(stockCode);
    return '收藏成功';
  }
}

class SpyFavoriteService implements FavoriteService {
  callCount: number = 0;
  lastStockCode: string = '';

  save(stockCode: string): Promise<void> {
    this.callCount += 1;
    this.lastStockCode = stockCode;
    return Promise.resolve();
  }
}

export default function favoriteManagerTest(): void {
  describe('FavoriteManager', () => {
    it('savesExactlyOnce', 0, async () => {
      const spyService: SpyFavoriteService = new SpyFavoriteService();
      const manager: FavoriteManager = new FavoriteManager(spyService);

      const result: string = await manager.add('600000');

      expect(spyService.callCount).assertEqual(1);
      expect(spyService.lastStockCode).assertEqual('600000');
      expect(result).assertEqual('收藏成功');
    });
  });
}

这里断言了三个对外可观察结果:保存恰好发生一次、参数正确、业务结果正确。测试没有关心 FavoriteManager 内部如何组织代码。

3~5 分钟练习

给 FavoriteManager 增加 addTwiceForDemo(stockCode):连续调用两次 save()。补一个测试,验证 Spy 的 callCount 为 2,且最后一次参数仍是传入的股票代码。

参考答案

ts
async addTwiceForDemo(stockCode: string): Promise<void> {
  await this.favoriteService.save(stockCode);
  await this.favoriteService.save(stockCode);
}

it('savesTwiceForDemo', 0, async () => {
  const spyService: SpyFavoriteService = new SpyFavoriteService();
  const manager: FavoriteManager = new FavoriteManager(spyService);

  await manager.addTwiceForDemo('600000');

  expect(spyService.callCount).assertEqual(2);
  expect(spyService.lastStockCode).assertEqual('600000');
});

与上一课的联系

上一课的 Fake 主要回答“依赖返回什么、被测逻辑输出什么”;本课的 Spy 进一步回答“依赖是否按契约被调用”。同一个测试替身可以同时承担可控返回值和调用记录,但断言应只覆盖真正重要的外部交互。