You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular测试问题:jasmine-marbles测试NGRX Effect返回空结果

NGRX Effect 文件下载测试失败排查

问题背景

我们有一个基于Angular + NGRX Store的项目,所有Effect均遵循「触发动作→API调用→成功动作」的逻辑,API调用基于Open API规范自动生成的客户端实现,所有API方法返回Observable。目前使用jasmine-marbles测试Effect,大部分用例正常,仅文件下载相关测试失败。

测试代码

it('loads a something file', () => {
  const action = SomethingActions.Get({ param: 's1' });

  actions$ = hot('--a-', { a: action });
  const api = cold('--b', { b: new Blob([TEST_DATA]) });
  const expected = cold('----c', {
    c: SomethingActions.GetSuccess({ param: 's1', records: EXPECTED_RESULT }),
  });

  when(fileServiceMock.getFile('something-s1.txt')).thenReturn(api);

  // effects.loadSomething$.subscribe((result) => {
  //   console.log(result);
  // });

  expect(effects.loadSomething$).toBeObservable(expected);

  verify(fileServiceMock.getFile('something-s1.txt')).once();
});

错误信息

取消注释订阅代码后,会在测试报错后打印出预期结果,错误提示:

Expect $.length = 0 to equal 1.

Effect 代码

public loadSomething$ = createEffect(() =>
this.actions$.pipe(
  ofType(SomethingActions.Get),
  mergeMap(({ param }) => {
    return fileService.getFile(`something-${param}.txt`)).pipe(
      mergeMap(async (blob) => await blob.text()),
      catchError(() => of(DEFAULT_DATA)),
      map((csv) => SomethingActions.GetSuccess({ spindle, records: parseData(csv) }))
    );
  }),
));

补充调试信息

在Effect中多处添加tap(x => console.log(x))调试,显示Effect在测试期间确实返回了预期结果,但测试仍不通过。


问题原因及解决方案

核心原因:异步Promise与Marbles虚拟时间调度不兼容

你的Effect中使用了mergeMap(async (blob) => await blob.text()),这里的async/await会将Observable流转换为真实异步Promise。而jasmine-marbles的toBeObservable依赖同步虚拟时间调度器,虚拟时间不会等待真实异步操作(比如Blob.text()返回的Promise)完成,导致Marbles测试判定流未发出任何值,从而抛出$.length = 0的错误。

手动订阅能看到结果,是因为手动订阅会等待真实异步任务执行完成,但Marbles测试的TestScheduler只会同步执行虚拟时间内的操作,不会处理真实异步逻辑。

解决方案

方案1:Mock Blob的异步行为

不要返回真实Blob对象,而是构造一个mock对象,将text()方法改为同步或可控的异步实现:

// 替换测试中的api定义
const mockBlob = {
  text: () => Promise.resolve(TEST_DATA)
} as unknown as Blob;
const api = cold('--b', { b: mockBlob });

方案2:将Promise转换为Observable(推荐)

修改Effect代码,用RxJS的from将Blob.text()返回的Promise转换为Observable,让整个流受RxJS调度器控制:

// 修改Effect中的mergeMap逻辑
mergeMap((blob) => from(blob.text())), // 用from把Promise转为Observable

这样整个流的时序完全由TestScheduler的虚拟时间管理,Marbles测试就能正确识别发出的值和时序。

方案3:用fakeAsync/tick替代Marbles测试(备选)

若不想修改代码或mock,可使用Angular的fakeAsync和tick()等待异步完成:

it('loads a something file', fakeAsync(() => {
  const action = SomethingActions.Get({ param: 's1' });
  const expectedAction = SomethingActions.GetSuccess({ param: 's1', records: EXPECTED_RESULT });
  let emittedAction: any;

  when(fileServiceMock.getFile('something-s1.txt')).thenReturn(cold('--b', { b: new Blob([TEST_DATA]) }));
  
  effects.loadSomething$.subscribe(action => emittedAction = action);
  
  actions$.next(action);
  tick(1000); // 等待异步操作完成
  
  expect(emittedAction).toEqual(expectedAction);
  verify(fileServiceMock.getFile('something-s1.txt')).once();
}));

此方法会失去Marbles测试的时序可视化优势,仅作为特殊场景下的备选。


内容的提问来源于stack exchange,提问作者Woozar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 04:02:41