Vue组合式函数内部函数的Vitest Spy失效问题排查
组合式函数中wait函数的Spy不生效问题解析
问题背景
我在用Vue、Vitest和Vue Test Utils做UI单元测试,想验证组合式函数useHelper中executeInBatches方法是否调用了内部的wait函数,但对wait的Spy始终不生效;不过Spywait内部调用的window.setTimeout却能正常工作。
组合式函数代码
export function useHelper() { async function wait(milliseconds: number): Promise<void> { return await new Promise<void>(resolve => setTimeout(() => resolve(), milliseconds)); } async function executeInBatches(delay = 0) { if (delay) { await wait(delay); } } return { executeInBatches, wait, }; }
失效的Spy测试代码
beforeEach(() => { vi.useFakeTimers({shouldAdvanceTime: true}); }); afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); }); it('should call function wait when delay is provided', async () => { const composable = useHelper(); const waitSpy = vi.spyOn(composable, 'wait'); const delay = 100; // 错误:直接调用未定义的全局函数,正确应为 composable.executeInBatches(delay) await executeInBatches(delay); vi.advanceTimersByTime(delay * 2); expect(waitSpy).toHaveBeenCalled(); });
有效的Spy setTimeout测试代码
it('should call setTimeout when delay is provided', async () => { const composable = useHelper(); const timeoutSpy = vi.spyOn(window, 'setTimeout'); const delay = 100; await composable.executeInBatches(delay); vi.advanceTimersByTime(delay * 2); expect(timeoutSpy).toHaveBeenCalled(); });
原因分析
测试代码的直接错误:你在测试中直接调用了
executeInBatches(delay),但该函数仅存在于useHelper返回的对象中,正确调用应为composable.executeInBatches(delay)。不过即使修正这个问题,Spywait仍会失效,核心原因是闭包作用域限制。闭包导致Spy失效的核心逻辑:
- 在
useHelper内部,executeInBatches直接引用的是作用域内定义的原始wait函数(闭包保留的引用)。 vi.spyOn(composable, 'wait')的本质是替换返回对象的wait属性为Spy包装后的函数,但executeInBatches内部调用的还是原始的wait函数,因此不会触发Spy。- 而Spy
window.setTimeout能生效,是因为wait内部调用的是全局作用域的setTimeout,Spy替换全局引用后,所有调用都会触发。
- 在
解决方案
方案一:修改组合式函数的内部调用逻辑
让executeInBatches调用对象上的wait方法,确保内部引用和外部Spy的是同一个函数:
export function useHelper() { const helpers = { async wait(milliseconds: number): Promise<void> { return await new Promise<void>(resolve => setTimeout(() => resolve(), milliseconds)); }, async executeInBatches(delay = 0) { if (delay) { await helpers.wait(delay); // 调用对象上的wait,而非直接引用作用域函数 } } }; return helpers; }
修改后,vi.spyOn(composable, 'wait')会替换helpers.wait的引用,executeInBatches调用的就是Spy包装后的函数,测试断言可正常触发。
方案二:通过行为验证替代直接Spy(推荐,无需修改业务代码)
如果不想改动业务代码,可以通过验证wait的副作用(延迟Promise.resolve)来间接测试executeInBatches是否调用了它:
it('should wait for specified delay when delay is provided', async () => { const composable = useHelper(); const delay = 100; const promise = composable.executeInBatches(delay); // 验证初始状态:Promise未resolve,计时器已创建 expect(promise).toBeInstanceOf(Promise); expect(vi.getTimerCount()).toBe(1); // 快进时间到延迟后 vi.advanceTimersByTime(delay); await promise; // 验证Promise已resolve,计时器已清除 expect(vi.getTimerCount()).toBe(0); });
这种方式聚焦于测试executeInBatches的实际行为,而非内部函数调用,更符合单元测试的行为驱动原则。
内容的提问来源于stack exchange,提问作者Vitalii Moholiuk
相关产品推荐
相关产品推荐

