You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
});

原因分析

  1. 测试代码的直接错误:你在测试中直接调用了executeInBatches(delay),但该函数仅存在于useHelper返回的对象中,正确调用应为composable.executeInBatches(delay)。不过即使修正这个问题,Spywait仍会失效,核心原因是闭包作用域限制。

  2. 闭包导致Spy失效的核心逻辑:

    • 在useHelper内部,executeInBatches直接引用的是作用域内定义的原始wait函数(闭包保留的引用)。
    • vi.spyOn(composable, 'wait')的本质是替换返回对象的wait属性为Spy包装后的函数,但executeInBatches内部调用的还是原始的wait函数,因此不会触发Spy。
    • 而Spywindow.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 09:39:51