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')
报错原因分析
- 重复注册MatDialogRef导致依赖注入异常:在TestBed的providers数组中重复添加了
{ provide: MatDialogRef, useValue: {} },会覆盖前面的matDialogRefSpy实例,虽然不是当前报错的直接原因,但会引发后续测试问题。 - 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
相关产品推荐
相关产品推荐

