Angular模态框单元测试报错:无法读取undefined的text属性
问题根因
这个报错核心原因非常明确:断言执行时(modalWindow1.childReference as any)._component值为undefined,根本没完成实例化。结合你提到的ProxyZone、Promise.then触发未捕获rejection的特征,本质就是测试用例的异步时序没有对齐,不存在复杂的逻辑bug:
- 模态框的动态组件创建、实例挂载是异步行为,不管是Angular动态组件的resolve过程、变更检测周期,还是模态框自身的初始化逻辑、入场动画,对应的任务都会排在Zone.js追踪的异步队列里
- 你写断言时没有等待上述异步任务执行完成,同步读取还没初始化的
_component属性,自然会触发Cannot read properties of undefined (reading 'text')错误 - 由于这个属性读取逻辑是在
Promise.then回调中执行(Zone.js用ProxyZone包裹追踪所有异步任务,所以日志里关联Zone显示为ProxyZone是正常现象),既没有提前等待Promise落定,也没有捕获异常,最终就抛出了Unhandled Promise rejection - 如果你已经用了异步测试包装器还出这个问题,基本都是写法疏漏:用
fakeAsync没执行tick/flush清空队列,用waitForAsync没等fixture.whenStable()resolve就执行断言。
排查思路
按顺序排查即可,不需要盲目改代码:
- 先查modal服务的open实现:定位modal.service.ts里的open方法,确认
modalWindow.childReference._component的赋值时机——是open方法同步执行时就赋值,还是要等动态组件加载完成、组件ngAfterViewInit钩子触发后才会赋值,同时确认open方法是否返回Promise/Observable来标记初始化完成 - 再查测试用例的异步控制逻辑:
- 用
fakeAsync包装的用例,检查断言前有没有调用tick()或flush()清空队列里的微任务、宏任务 - 用
waitForAsync包装的用例,检查断言是不是写在await fixture.whenStable()之后 - 如果直接依赖open方法的返回值,检查断言是不是写在subscribe回调或者await之后,有没有同步写在open()调用的下一行
- 用
- 断点验证:在第288行断言位置打debug断点,测试运行到断点时直接查看
modalWindow1.childReference的实际值,确认_component是否真的为undefined,同时顺着调用栈找上游有没有遗漏await的异步调用 - 排除mock逻辑问题:如果测试中对模态框引用、动态组件引用做了mock,检查mock逻辑是不是漏了
_component的初始化赋值,有没有返回空对象的情况。
可行修复方案
根据你当前测试用例的写法选对应方案即可:
方案1:fakeAsync+flush控制时序(推荐,执行速度最快)
把测试用例用fakeAsync包装,在调用open之后、断言之前调用flush()清空所有排队的异步任务,再触发一次变更检测:
it('should render text modal with passed config', fakeAsync(() => { // 前置准备逻辑:初始化测试床、注入ModalService、构造config1 const modalWindow1 = modalService.open(config1); flush(); // 清空所有微任务、宏任务,等待动态组件初始化完成 fixture.detectChanges(); // 触发视图更新 // 执行断言 expect((modalWindow1.childReference as any)._component.text).toEqual(config1.text); }));
如果模态框带定时入场动画,把flush()换成对应动画时长的tick(动画毫秒数)即可。
方案2:waitForAsync+whenStable等待视图稳定
如果不想用fakeAsync,可以等Angular整个变更检测队列完全稳定后再执行断言:
it('should render text modal with passed config', waitForAsync(async () => { const modalWindow1 = modalService.open(config1); await fixture.whenStable(); fixture.detectChanges(); expect((modalWindow1.childReference as any)._component.text).toEqual(config1.text); }));
方案3:直接等待open方法的异步返回
如果你的modalService.open本身就返回Promise,在动态组件实例化完成、_component挂载好之后才会resolve,直接await这个返回值即可:
it('should render text modal with passed config', async () => { const modalWindow1 = await modalService.open(config1); // 等待模态框初始化完成 fixture.detectChanges(); expect((modalWindow1.childReference as any)._component.text).toEqual(config1.text); });
兜底修复(针对mock逻辑错误)
如果排查后发现是测试用的mock对象漏了_component的初始化,直接补全mock逻辑即可,不要用空对象模拟动态组件引用:
// 补全mock的ModalWindow逻辑 class MockModalWindow { childReference = { _component: { text: '' } } }
内容的提问来源于stack exchange,提问作者Stephen
相关产品推荐
相关产品推荐

