Angular Material Dialog测试报错:Cannot read properties of undefined (reading 'push')
错误信息
TypeError: Cannot read properties of undefined (reading 'push')
at MatDialog.open (node_modules/@angular/material/fesm2022/dialog.mjs:598:26)
at MatDialog.open (node_modules/@angular/material/fesm2022/dialog.mjs:598:26)
at UserComponent.deleteUser (src/app/components/user/user.component.ts:108:35)
at UserContext.apply (src/app/components/user/user.component.spec.ts:147:15)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:369:28)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:2081:39)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:368:34)
at ZoneImpl.run (node_modules/zone.js/fesm2015/zone.js:111:43)
at runInTestZone (node_modules/zone.js/fesm2015/zone-testing.js:216:38)
at UserContext. (node_modules/zone.js/fesm2015/zone-testing.js:234:32)
at
at UserContext.apply (src/app/components/user/user.component.spec.ts:147:15)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:369:28)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:2081:39)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:368:34)
at ZoneImpl.run (node_modules/zone.js/fesm2015/zone.js:111:43)
at runInTestZone (node_modules/zone.js/fesm2015/zone-testing.js:216:38)
UserComponent相关代码
deleteUser(userId: number, userNickname: string): void { const dialogRef = this.dialog.open(DeleteDialogComponent, { height: '400px', width: '550px', data: { userId, userNickname }, }); dialogRef.afterClosed().subscribe(result => { if (result) { const subscription = this.userService.deleteUser(userId).subscribe({ next: () => { console.log("Ok!") }, error: error => { } }); } }); }
原测试代码
describe('UserComponent', () => { let component: UserComponent; let fixture: ComponentFixture<UserComponent>; let userServiceMock: jasmine.SpyObj<UserService>; let routerMock: jasmine.SpyObj<Router>; let dialogSpy: jasmine.SpyObj<MatDialog>; beforeEach(async () => { userServiceMock = jasmine.createSpyObj<UserService>('UserService', { deleteUser: of(1) }); routerMock = jasmine.createSpyObj<Router>('Router', ['navigate']); dialogSpy = jasmine.createSpyObj('MatDialog', ['open']); await TestBed.configureTestingModule({ imports: [UserComponent, MatDialogModule, BrowserAnimationsModule], providers: [ { provide: UserService, useValue: userServiceMock }, { provide: Router, useValue: routerMock }, { provide: MatDialog, useValue: dialogSpy } ] }).compileComponents(); fixture = TestBed.createComponent(UserComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should open modal and delete user', () => { const userId = 1; const userNickname = 'JohnSmith'; component.deleteUser(userId, userNickname); const dialogRef = dialogSpy.open.calls.mostRecent().returnValue; dialogRef.afterClosed = jasmine.createSpy().and.returnValue(of(true)); component.deleteUser(userId, userNickname); expect(userServiceMock.deleteUser).toHaveBeenCalledWith(userId); }); });
问题分析与解决方案
核心问题
dialogSpy仅Mock了open方法,但MatDialog内部维护的openDialogs数组未初始化,导致调用open时执行this.openDialogs.push时报错。- 测试逻辑错误:第一次调用
deleteUser时,open方法返回undefined,后续修改返回值的操作无效,且重复调用deleteUser不符合测试预期。
修正后的测试代码
describe('UserComponent', () => { let component: UserComponent; let fixture: ComponentFixture<UserComponent>; let userServiceMock: jasmine.SpyObj<UserService>; let routerMock: jasmine.SpyObj<Router>; let dialogSpy: jasmine.SpyObj<MatDialog>; // 提前创建dialogRef的Spy对象 let dialogRefSpy: jasmine.SpyObj<any>; beforeEach(async () => { userServiceMock = jasmine.createSpyObj<UserService>('UserService', { deleteUser: of(1) }); routerMock = jasmine.createSpyObj<Router>('Router', ['navigate']); // 初始化dialogRef的Spy,预设afterClosed方法的返回值 dialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['afterClosed']); dialogRefSpy.afterClosed.and.returnValue(of(true)); // 创建MatDialog的Spy,同时初始化openDialogs数组,并让open返回预设的dialogRefSpy dialogSpy = jasmine.createSpyObj('MatDialog', ['open'], { openDialogs: [] }); dialogSpy.open.and.returnValue(dialogRefSpy); await TestBed.configureTestingModule({ imports: [UserComponent, MatDialogModule, BrowserAnimationsModule], providers: [ { provide: UserService, useValue: userServiceMock }, { provide: Router, useValue: routerMock }, { provide: MatDialog, useValue: dialogSpy } ] }).compileComponents(); fixture = TestBed.createComponent(UserComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should open modal and delete user when dialog confirms', () => { const userId = 1; const userNickname = 'JohnSmith'; // 调用目标方法 component.deleteUser(userId, userNickname); // 验证对话框是否按预期打开 expect(dialogSpy.open).toHaveBeenCalledWith(DeleteDialogComponent, { height: '400px', width: '550px', data: { userId, userNickname }, }); // 验证用户服务的deleteUser方法是否被触发 expect(userServiceMock.deleteUser).toHaveBeenCalledWith(userId); }); });
关键修改说明
- 初始化
openDialogs数组:在创建dialogSpy时,通过第三个参数给MatDialog实例添加openDialogs: [],解决内部push操作的undefined问题。 - 预设
dialogRef的返回值:提前创建包含afterClosed方法的dialogRefSpy,并让dialogSpy.open直接返回该对象,确保调用deleteUser时能正确获取到有效的dialogRef。 - 调整测试逻辑:去掉重复调用
deleteUser的代码,先设置好所有Spy的返回值,再执行目标方法,最后依次验证对话框打开和用户删除的行为。
内容的提问来源于stack exchange,提问作者Polkov

