DEBUG.WIKI
晚安云;香港 CN2 ¥15/月
PanstarCloud:部署应用,接入 AI,云服务器与 AI 网关
野草云;香港 200Mbps 峰值;不限月流量 · BGP VPS;查看套餐 →

Vitest mockReset 后真实生成器不能迭代,检查 automock 共享 prototype

自动 mock 后再读取真实模块仍报 async iterable 错误,可能是 mock 与原生成器共用了 prototype。改用局部手写工厂时仍要检查真实模块。

对模块做 automock 后调用 mockReset(),随后用 vi.importActual() 取得真实导出,仍报 is not a function or its return value is not async iterable。如果导出是生成器或异步生成器,检查真实函数的 prototype 是否已经被 mock 操作改写。

这条问题明确出现在 Vitest 5.0.0 和 5.0.5。例如 node:timers/promises 的异步迭代 setInterval,自动 mock 后重置 mock,再调用真实版本可能失去异步迭代能力。它不等于 importActual 又返回了同一个 mock。

原函数也可能被共享对象污染

受影响的自动 mock 过程会把某些非普通对象原样复制到 mock。生成器函数的 prototype 属于这种路径,mock 与原函数因而持有同一份对象。后续重置或设置实现时,spy 层调整 mock prototype 的父级,也就改变了真实函数那份对象。

因此,仅换成 vi.importActual() 无法恢复已经变化的原型。spy: true 也不能在这条路径上被当成安全替代;它仍可能经过相同原型处理。

保留原测试、完整调用顺序和实际版本,比较自动 mock 前后 Object.getPrototypeOf(generator.prototype)。如果业务函数本身不是生成器,继续检查导出是否具有自定义非普通 prototype,不能只看函数名判断。

用明确的模块工厂绕开自动遍历

可以只替换需要控制的导出,避免 automock 整个真实生成器。下面针对内置模块的工厂保留其他真实导出,并提供一个独立的异步生成器 mock:

import { expect, test, vi } from 'vitest';
import { setInterval } from 'node:timers/promises';

vi.mock(import('node:timers/promises'), async (importOriginal) => {
  const actual = await importOriginal();
  return {
    ...actual,
    setInterval: vi.fn(async function* () { yield 'tick'; }),
  };
});

test('uses the controlled interval', async () => {
  const values = [];
  for await (const value of setInterval(1)) values.push(value);
  expect(values).toEqual(['tick']);
});

示例只输出一次,真实定时器的间隔、取消、错误和持续迭代行为并没有被模拟。业务测试需要这些语义时,应在 mock 中明确实现或保留真实依赖,不能把这个简化工厂套到所有定时器测试。

工厂中的新 mock 不依赖真实生成器的 prototype。仍需审查 reset 行为:重置后实现可能被清除,测试是否重新设置实现,要按实际 mock API 与用例生命周期安排。

用干净的进程验证真实导出

原型已经被旧测试污染的进程,应结束后从新进程开始验证。不要在测试里手动重接全局或 Node 内置函数的原型,其他代码可能仍持有被影响的对象。

验收包括 mock 返回预期序列,真实模块的原型链在操作前后不变,以及真实异步生成器仍能正常迭代并结束。再运行相关测试组,检查没有通过调整顺序把污染转移到后一个文件。

已核对的 5.0.5 正式 automocker 和 spy 代码仍有该共享路径,不以关闭状态承诺补丁已发布。采用后续正式修复后,用原来的 automock 加 reset 顺序回归,再评估是否保留手写工厂。保存锁文件和旧测试,回退不能只恢复工具而遗留不匹配的 mock 行为。