如何在Jasmine/Angular中用document spy覆盖ngOnInit内的setTimeout函数?
解决Angular组件setTimeout单元测试无法进入代码块的问题
我来帮你拆解问题,一步步搞定这个测试覆盖的难题:
核心问题分析
你的测试没进入setTimeout代码块,主要是三个原因在搞鬼:
tick()没传延迟时间,默认只推进微任务队列,不会触发50ms后的宏任务(也就是你的setTimeout);- 测试环境里没有真实的
#field-${id}和#${id}DOM元素,document.querySelector会返回null,就算进入代码块也跑不通后续逻辑,而且你没对这个方法做模拟; fixture.detectChanges()的调用时机错了,得先触发ngOnInit,再推进时间。
解决方案步骤
1. 模拟DOM元素和document.querySelector
首先得造一些模拟的DOM元素,让document.querySelector返回这些假对象,这样才能覆盖getState()里的逻辑。用Jasmine的spy就能轻松搞定:
// 创建模拟的tooltip元素,包含需要的方法和属性 const mockTooltip = { querySelector: jasmine.createSpy('tooltipQuerySelector').and.returnValue({ clientHeight: 20 }), style: {} }; // 创建模拟的容器元素 const mockContainer = { querySelector: jasmine.createSpy('containerQuerySelector').and.returnValue(mockTooltip) }; // 创建模拟的输入框元素,监听addEventListener调用 const mockInputField = { addEventListener: jasmine.createSpy('inputAddEventListener') }; // 监听document.querySelector,根据不同选择器返回对应的模拟元素 spyOn(document, 'querySelector').and.callFake((selector) => { if (selector === '#field-fieldId') { return mockContainer; } else if (selector === '#fieldId') { return mockInputField; } return null; });
2. 修正fakeAsync测试的执行顺序
调整测试代码的执行流程,确保先触发ngOnInit,再推进时间到setTimeout的延迟后,最后做断言:
it('should initialize container, inputField and call getState correctly', fakeAsync(() => { const fixture = TestBed.createComponent(TooltipComponent); const componentInst = fixture.componentInstance; componentInst.id = 'fieldId'; // 第一步:触发组件的ngOnInit生命周期钩子 fixture.detectChanges(); // 第二步:推进50ms,执行setTimeout内部的代码 tick(50); // 断言container和inputField是否正确赋值 expect(componentInst.container).toBe(mockContainer); expect(componentInst.inputField).toBe(mockInputField); // 验证getState是否为输入框绑定了focus和blur事件 expect(mockInputField.addEventListener).toHaveBeenCalledTimes(2); expect(mockInputField.addEventListener).toHaveBeenCalledWith('focus', jasmine.any(Function)); expect(mockInputField.addEventListener).toHaveBeenCalledWith('blur', jasmine.any(Function)); // 额外测试focus事件的逻辑是否正确 const focusHandler = mockInputField.addEventListener.calls.argsFor(0)[1]; focusHandler(); expect(mockTooltip.style.height).toBe('20px'); // 额外测试blur事件的逻辑是否正确 const blurHandler = mockInputField.addEventListener.calls.argsFor(1)[1]; blurHandler(); expect(mockTooltip.style.height).toBe('0px'); }));
关键细节说明
tick(50):必须传入和setTimeout一致的延迟时间,Angular的fakeAsync才能精准推进时间,触发对应的宏任务;fixture.detectChanges():第一次调用会触发组件的ngOnInit,所以一定要放在tick之前;- 模拟
addEventListener:不仅要验证事件是否绑定,还要手动调用事件处理函数,确保样式修改的逻辑符合预期; callFake:根据不同选择器返回不同模拟元素,更贴近真实业务场景的逻辑。
这样调整后,你的测试就能完整覆盖setTimeout里的代码块,以及getState()里的事件绑定和处理逻辑了。
内容的提问来源于stack exchange,提问作者hzrcan
相关产品推荐
相关产品推荐

