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

RxJS switchMap内Jasmine Spy异常:unconfirm未被调用

Angular单元测试中unconfirm spy未被捕获的问题

问题背景

我开发了一个带事件确认与撤销确认功能的Angular组件,功能运行正常,但编写单元测试时遇到异常:Jasmine能捕获confirm方法的spy调用,却无法捕获unconfirm的调用。控制台已输出tap操作中的日志"called",但测试断言失败。

核心组件代码

constructor(private service: Service, private snackbarService: MatSnackBar) {}

confirm(id: number): void {
    this.service.confirm(id).pipe(
      switchMap(success => {
        const ref = this.snackbarService.open('Confirmed successfully', 'Undo confirm');
        return forkJoin([of(success), ref.afterDismissed()]);
      }),
      filter(([cancelable, dismiss]) => cancelable && dismiss.dismissedByAction),
      switchMap(() => this.service.unconfirm(id)),
      tap(() => console.log('called'))
    ).subscribe(() => {
      this.snackbarService.open('Undone successfully', 'Close');
    });
}

单元测试代码

beforeEach(async () => {
    const serviceMock = {
      confirm: (id: number) => of(true),
      unconfirm: (id: number) => of(true)
    };
    await TestBed.configureTestingModule({
      declarations: [TestComponent],
      imports: [
        BrowserAnimationsModule,
        MatSnackBarModule
      ],
      providers: [
        { provide: Service, useValue: serviceMock }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(TestComponent);
    component = fixture.componentInstance;
});

it('when confirming and canceling action, should show action canceled success snackbar', () => {
    const service = TestBed.inject(Service);
    spyOn(service, 'confirm').and.callThrough();
    spyOn(service, 'unconfirm').and.callThrough();

    const id = 1;

    component.confirm(id);

    expect(service.confirm).toHaveBeenCalledOnceWith(id);
    let snackbar = document.querySelector('simple-snack-bar');
    expect(snackbar).toBeTruthy();

    snackbar?.querySelector('button')?.click();
    fixture.detectChanges();
    expect(service.unconfirm).toHaveBeenCalledOnceWith(id);
});

报错信息

Expected spy unconfirm to have been called only once, and with given args:
[ 1 ]
But it was never called.

原因分析

问题核心在于异步流的处理逻辑:

  1. 组件中ref.afterDismissed()返回的是异步Observable,点击撤销按钮后,该Observable的触发、后续的filter和switchMap操作都会进入异步队列执行。
  2. 原测试是同步执行的,点击按钮后立即执行断言,此时异步队列中的unconfirm调用还未完成,导致spy未捕获到该调用;而测试结束后异步任务才执行,因此控制台能输出"called"但测试已判定失败。

解决方案

使用Angular测试提供的fakeAsync和tick来同步处理异步流,确保所有异步操作完成后再执行断言:

修改后的测试代码:

it('when confirming and canceling action, should show action canceled success snackbar', fakeAsync(() => {
    const service = TestBed.inject(Service);
    spyOn(service, 'confirm').and.callThrough();
    spyOn(service, 'unconfirm').and.callThrough();

    const id = 1;

    component.confirm(id);
    tick(); // 推进confirm的Observable流完成,渲染出snackbar

    expect(service.confirm).toHaveBeenCalledOnceWith(id);
    let snackbar = document.querySelector('simple-snack-bar');
    expect(snackbar).toBeTruthy();

    snackbar?.querySelector('button')?.click();
    tick(); // 触发afterDismissed的Observable,推进整个流完成
    fixture.detectChanges();

    expect(service.unconfirm).toHaveBeenCalledOnceWith(id);
}));

代码说明

  • fakeAsync:创建伪造的异步环境,将所有异步操作转为同步执行。
  • tick():模拟时间流逝,触发所有待处理的异步任务,确保Observable链中的所有操作都执行完毕后再进行断言。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 09:25:11