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

Angular 16 Jasmine测试报错:预期Spy被调用但未触发

Angular 16 Jasmine单元测试问题:ConfirmationService.confirm未被调用

我在Angular 16项目中用Jasmine编写单元测试时,多个组件遇到同一问题:测试关闭侧边栏的提示逻辑时,测试文件始终报错:Error: Expected spy ConfirmationService.confirm to have been called with [ Object(...) ] but it was never called。尝试过使用async/fakeAsync都无效,且相同逻辑在其他组件测试正常,代码调用也未发现差异,求解决办法。


SPEC.TS代码

describe('EditDeclarationComponent', () => {
  let component: EditDeclarationComponent;
  let fixture: ComponentFixture<EditDeclarationComponent>;
  let installationServiceSpy: jasmine.SpyObj<InstallationService>;
  let confirmationServiceSpy: jasmine.SpyObj<ConfirmationService>;
  let declarationServiceSpy: jasmine.SpyObj<DeclarationService>;

beforeEach(async () => {
    installationServiceSpy = jasmine.createSpyObj('InstallationService', [
      'getInstallationsType',
      'existInstallationNPM',
      'saveInstalacion',
    ]);
    confirmationServiceSpy = jasmine.createSpyObj('ConfirmationService', [
      'confirm',
    ]);
    declarationServiceSpy = jasmine.createSpyObj('DeclarationService', [
      'updateDeclaration',
    ]);
    await TestBed.configureTestingModule({
     declarations: [
      EditDeclarationComponent,
      NumberPipe,
      SgIconComponent,
      SgFormAddDeclarationDestinationComponent,
      SgButtonComponent,
    ],
      providers: [
        { provide: InstallationService, useValue: installationServiceSpy },
        MessageService,
        { provide: ConfirmationService, useValue: confirmationServiceSpy },
        { provide: DeclarationService, useValue: declarationServiceSpy },
      ],
      imports: [
        HttpClientModule,
        DialogModule,
        DividerModule,
        ConfirmDialogModule,
        ToastModule,
        AutoCompleteModule,
        InputTextModule,
        FormsModule,
        ReactiveFormsModule,
      ],
    }).compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(EditDeclarationComponent);

    component = fixture.componentInstance;

    installationServiceSpy = TestBed.inject(
      InstallationService
    ) as jasmine.SpyObj<InstallationService>;

    confirmationServiceSpy = TestBed.inject(
      ConfirmationService
    ) as jasmine.SpyObj<ConfirmationService>;

    declarationServiceSpy = TestBed.inject(
      DeclarationService
    ) as jasmine.SpyObj<DeclarationService>;

    installationServiceSpy.getInstallationsType.and.returnValue(of(response));

    fixture.detectChanges();
  });

测试用例代码

it('should set instalacionNew and dialogNewInstalacion correctly when accepted', () => {
      const mockConfirmationResponse: any = {
        accept: (callback: Function) => {
          callback();
        },
      };

      confirmationServiceSpy.confirm.and.callFake((options: any) =>
        options.accept(mockConfirmationResponse.accept)
      );

      component.onCancelNewInstallation();

      expect(component.instalacionNew).toEqual({} as Installation);

      expect(component.dialogNewInstalacion).toBe(false);

      expect(confirmationServiceSpy.confirm).toHaveBeenCalledWith({
        message: 'Are you sure about exiting the reinstall window?',
        header: 'Exit new installation window?',
        icon: 'pi pi-info-circle',
        acceptLabel: 'Yes',
        rejectLabel: 'No',
        accept: jasmine.any(Function),
      });

    });

组件TS代码

instalacionNew: Installation = {} as Installation;
dialogNewInstalacion: boolean = false;
// Installation是一个包含对象的model.ts文件

 constructor(
    private confirmationService: ConfirmationService,
    private declarationService: DeclarationService,
    private messageService: MessageService,
    private cd: ChangeDetectorRef,
    private installationService: InstallationService
  ) {}

 onCancelNewInstallation() {
    this.confirmationService.confirm({
      message: 'Are you sure about exiting the reinstall window?',
      header: 'Exit new installation window?',
      icon: 'pi pi-info-circle',
      acceptLabel: 'Yes',
      rejectLabel: 'No',
      accept: () => {
        this.instalacionNew = {} as Installation;
        this.dialogNewInstalacion = false;
      },
    });
  }

问题排查与解决办法

1. 修复Spy对象的重复赋值问题

你在第一个beforeEach(async)中创建了confirmationServiceSpy,但在第二个beforeEach里又通过TestBed.inject重新赋值了一次,可能导致Spy没有正确关联到组件实例。移除第二个beforeEach中的Spy重新注入代码:

// 修改后的第二个beforeEach
beforeEach(() => {
  fixture = TestBed.createComponent(EditDeclarationComponent);
  component = fixture.componentInstance;
  // 移除此处三个Spy的重新注入代码
  installationServiceSpy.getInstallationsType.and.returnValue(of(response));
  fixture.detectChanges();
});

2. 调整测试中Spy的调用逻辑

当前callFake的写法有误,组件的accept回调不需要参数,直接执行即可。同时用jasmine.objectContaining代替严格对象匹配,避免因引用或额外属性导致匹配失败:

it('should set instalacionNew and dialogNewInstalacion correctly when accepted', () => {
  // 直接模拟confirm方法,执行传入的accept回调
  confirmationServiceSpy.confirm.and.callFake((options: any) => {
    options.accept();
  });

  component.onCancelNewInstallation();

  expect(component.instalacionNew).toEqual({} as Installation);
  expect(component.dialogNewInstalacion).toBe(false);
  expect(confirmationServiceSpy.confirm).toHaveBeenCalledWith(jasmine.objectContaining({
    message: 'Are you sure about exiting the reinstall window?',
    header: 'Exit new installation window?',
    icon: 'pi pi-info-circle',
    acceptLabel: 'Yes',
    rejectLabel: 'No',
    accept: jasmine.any(Function)
  }));
});

3. 排查组件生命周期的Service覆盖问题

检查组件的ngOnInit或其他生命周期钩子中是否重新实例化了ConfirmationService,如果有会导致Spy失效。

4. 按需添加变更检测(可选)

如果测试中发现属性未更新,可在调用component.onCancelNewInstallation()后添加fixture.detectChanges()触发变更检测。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 09:02:08