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

Angular单元测试配置间谍后仍失败:subscribe属性读取错误

单元测试报错排查:TypeError: Cannot read properties of undefined (reading 'subscribe')

问题背景

组件代码(ChangeRolesDialogComponent)

constructor(
  private readonly dialogRef: MatDialogRef<ChangeRolesDialogComponent>,
  private readonly rolesService: RolesService,
  private readonly usersService: UsersService
) {}

ngOnInit(): void {
  this.rolesService.getAllRoles().subscribe((roles: Role[]) => {
    this.roles = roles;
  });
}

测试配置(.spec文件)

fdescribe('ChangeRolesDialogComponent', () => {
  let component: ChangeRolesDialogComponent;
  let fixture: ComponentFixture<ChangeRolesDialogComponent>;

  let rolesServiceSpy: jasmine.SpyObj<RolesService>;
  let usersServiceSpy: jasmine.SpyObj<UsersService>;
  let matDialogRefSpy: jasmine.SpyObj<MatDialogRef<ChangeRolesDialogComponent>>;

  beforeEach(async () => {
    rolesServiceSpy = jasmine.createSpyObj('RolesService', ['getAllRoles']);
    usersServiceSpy = jasmine.createSpyObj('UsersService', [
      'getCurrentUserAuthentication',
    ]);

    matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['close']);
    await TestBed.configureTestingModule({
      declarations: [ChangeRolesDialogComponent],
      providers: [
        { provide: RolesService, useValue: rolesServiceSpy },
        { provide: UsersService, useValue: usersServiceSpy },
        { provide: MatDialogRef, useValue: matDialogRefSpy },
        { provide: MatDialogRef, useValue: {} },
      ],

测试用例

it('should get all roles on init', () => {
  const roles: Role[] = [Role.ADMINISTRATORS, Role.PRICING_ADMIN];
  rolesServiceSpy.getAllRoles.and.returnValue(of(roles));

  component.ngOnInit();

  expect(component.roles).toEqual(roles);
});

报错信息

should get all roles on init FAILED
TypeError: Cannot read properties of undefined (reading 'subscribe')

报错原因分析

  1. 重复注册MatDialogRef导致依赖注入异常:在TestBed的providers数组中重复添加了{ provide: MatDialogRef, useValue: {} },会覆盖前面的matDialogRefSpy实例,虽然不是当前报错的直接原因,但会引发后续测试问题。
  2. ngOnInit提前执行,Spy未设置返回值:如果在beforeEach中创建组件后调用了fixture.detectChanges(),会自动触发ngOnInit,此时测试用例还未给rolesServiceSpy.getAllRoles()设置返回值,导致该方法返回undefined,调用subscribe时抛出错误。

修复步骤

步骤1:移除重复的MatDialogRef提供者

在TestBed的providers中删除重复的配置,只保留使用matDialogRefSpy的项:

providers: [
  { provide: RolesService, useValue: rolesServiceSpy },
  { provide: UsersService, useValue: usersServiceSpy },
  { provide: MatDialogRef, useValue: matDialogRefSpy },
],

步骤2:控制ngOnInit的执行时机

修改beforeEach逻辑,只创建组件实例,不提前触发ngOnInit:

beforeEach(async () => {
  rolesServiceSpy = jasmine.createSpyObj('RolesService', ['getAllRoles']);
  usersServiceSpy = jasmine.createSpyObj('UsersService', ['getCurrentUserAuthentication']);
  matDialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['close']);

  await TestBed.configureTestingModule({
    declarations: [ChangeRolesDialogComponent],
    providers: [
      { provide: RolesService, useValue: rolesServiceSpy },
      { provide: UsersService, useValue: usersServiceSpy },
      { provide: MatDialogRef, useValue: matDialogRefSpy },
    ],
  }).compileComponents();
});

beforeEach(() => {
  fixture = TestBed.createComponent(ChangeRolesDialogComponent);
  component = fixture.componentInstance;
  // 不要在此处调用fixture.detectChanges()
});

步骤3:确保测试用例先设置Spy返回值再执行ngOnInit

保持测试用例逻辑,或改用fixture.detectChanges()触发ngOnInit(需先设置返回值):

it('should get all roles on init', () => {
  const roles: Role[] = [Role.ADMINISTRATORS, Role.PRICING_ADMIN];
  rolesServiceSpy.getAllRoles.and.returnValue(of(roles));

  // 方式1:手动调用ngOnInit
  component.ngOnInit();
  // 方式2:通过detectChanges触发ngOnInit
  // fixture.detectChanges();

  expect(component.roles).toEqual(roles);
});

验证修复

运行测试后,rolesServiceSpy.getAllRoles()会返回合法的Observable,subscribe可正常执行,组件roles属性会被正确赋值,测试即可通过。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 03:44:58