第 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 隔离两条路径的测试状态。