第 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 验证全部成功时的阶段顺序;本课保持同一套阶段信号,把第二阶段改为可控拒绝,补齐多阶段流程的中间失败契约。