Vi
Vitest 通过其 vi
辅助工具提供实用功能。你可以在全局范围内访问它(当 globals 配置 启用时),或者直接从 vitest
导入:
import { vi } from 'vitest';
模拟模块
本节介绍模拟模块时可用的 API。需要注意的是,Vitest 不支持模拟通过 require()
导入的模块。
vi.mock
- 类型:
(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void
- 类型:
<T>(path: Promise<T>, factory?: MockOptions | ((importOriginal: () => T) => T | Promise<T>)) => void
用另一个模块替换从指定 path
导入的所有模块。你可以在路径中使用已配置的 Vite 别名。vi.mock
的调用会被提升,因此调用位置不会影响其执行。它总是在所有导入之前执行。如果你需要引用 vi.mock
作用域之外的变量,你可以在 vi.hoisted
中定义它们,并在 vi.mock
中引用。
WARNING
vi.mock
仅适用于使用 import
关键字导入的模块。注意:不适用于 require
。
为实现提升,Vitest 会静态分析你的文件。这意味着无法使用未直接从 vitest
包(例如,从某个实用文件)导入的 vi
。请使用从 vitest
导入的 vi
来使用 vi.mock
,或者启用 globals
配置选项。
Vitest 不会模拟在 setup 文件中导入的模块,因为在测试文件运行时它们已被缓存。在运行测试文件之前清除所有模块缓存,你可以在 vi.hoisted
中调用 vi.resetModules()
。
如果定义了 factory
函数,所有导入都将返回其结果。Vitest 只调用一次工厂函数并缓存所有后续导入的结果,直到调用 vi.unmock
或 vi.doUnmock
。
与 jest
不同,工厂函数可以是异步的。你可以使用 vi.importActual
或一个辅助函数,将工厂函数作为第一个参数传入,并在其内部获取原始模块。
你还可以提供一个带有 spy
属性的对象,而不是工厂函数。如果 spy
为 true
,则 Vitest 将照常自动模拟模块,但它不会覆盖导出的实现。如果你只是想断言导出的方法被另一个方法正确调用,这会非常有用。
import { calculator } from './src/calculator.ts';
vi.mock('./src/calculator.ts', { spy: true });
// 调用原始实现,
// 但允许稍后断言其行为
const result = calculator(1, 2);
expect(result).toBe(3);
expect(calculator).toHaveBeenCalledWith(1, 2);
expect(calculator).toHaveReturned(3);
Vitest 还支持在 vi.mock
和 vi.doMock
方法中使用 Promise 模块路径而不是字符串,以获得更好的 IDE 支持。当文件移动时,路径会自动更新,并且 importOriginal
会自动继承类型。使用此签名还将强制工厂函数的返回类型与原始模块兼容(保持导出可选)。
// @filename: ./path/to/module.js
export declare function total(...numbers: number[]): number;
// @filename: test.js
import { vi } from 'vitest';
// ---cut---
vi.mock(import('./path/to/module.js'), async importOriginal => {
const mod = await importOriginal(); // 类型自动推断
// ^?
return {
...mod,
// 替换一些导出
total: vi.fn(),
};
});
在底层,Vitest 仍然基于字符串而非模块对象进行操作。
但是,如果你在 tsconfig.json
中使用 TypeScript 并配置了路径别名,编译器将无法正确解析导入类型。 为了使其工作,请确保将所有别名导入替换为其相应的相对路径。 例如,使用 import('./path/to/module.js')
而不是 import('@/module')
。
WARNING
vi.mock
会被提升(换句话说,移动)到文件顶部。这意味着无论你将其编写在何处(例如在 beforeEach
或 test
中),它实际上都会在此之前被调用。
这也意味着你不能在工厂函数中使用在工厂函数外部定义的任何变量。
如果你需要在工厂函数中使用变量,请尝试 vi.doMock
。它的工作方式相同,但不会被提升。请注意,它只模拟后续导入。
如果 vi.hoisted
方法在 vi.mock
之前声明,你也可以引用它定义的变量:
import { namedExport } from './path/to/module.js';
const mocks = vi.hoisted(() => {
return {
namedExport: vi.fn(),
};
});
vi.mock('./path/to/module.js', () => {
return {
namedExport: mocks.namedExport,
};
});
vi.mocked(namedExport).mockReturnValue(100);
expect(namedExport()).toBe(100);
expect(namedExport).toBe(mocks.namedExport);
WARNING
如果你正在模拟一个带有默认导出的模块,你需要在返回的工厂函数对象中提供一个 default
键。这是一个 ES 模块特有的限制;因此,jest
文档可能有所不同,因为 jest
使用 CommonJS 模块。例如,
vi.mock('./path/to/module.js', () => {
return {
default: { myDefaultKey: vi.fn() },
namedExport: vi.fn(),
// 等等...
};
});
如果 __mocks__
文件夹与你正在模拟的文件位于同一目录下,并且未提供工厂函数,Vitest 将尝试在 __mocks__
子文件夹中查找同名文件并将其用作实际模块。如果你正在模拟依赖项,Vitest 将尝试在项目 根目录(默认为 process.cwd()
)中查找 __mocks__
文件夹。你可以通过 deps.moduleDirectories
配置选项指定依赖项的位置。
例如,你有以下文件结构:
- __mocks__
- axios.js
- src
__mocks__
- increment.js
- increment.js
- tests
- increment.test.js
如果你在测试文件中调用 vi.mock
而不提供 factory 或选项,它将在 __mocks__
文件夹中查找文件以用作模块:
import { vi } from 'vitest';
// axios 是 `__mocks__/axios.js` 的默认导出
import axios from 'axios';
// increment 是 `src/__mocks__/increment.js` 的命名导出
import { increment } from '../increment.js';
vi.mock('axios');
vi.mock('../increment.js');
axios.get(`/apples/${increment(1)}`);
WARNING
请留意,如果你不调用 vi.mock
,模块不会自动模拟。要复现 Jest 的自动模拟行为,你可以在 setupFiles
中为每个所需模块调用 vi.mock
。
如果没有 __mocks__
文件夹或未提供工厂函数,Vitest 将导入原始模块并自动模拟其所有导出。有关应用的规则,请参阅算法。
vi.doMock
- 类型:
(path: string, factory?: MockOptions | ((importOriginal: () => unknown) => unknown)) => void
- 类型:
<T>(path: Promise<T>, factory?: MockOptions | ((importOriginal: () => T) => T | Promise<T>)) => void
与 vi.mock
相同,但它不会被提升到文件顶部,因此你可以在全局文件作用域内引用变量。该模块的下一次动态导入将被模拟。
WARNING
这不会模拟在此方法调用之前已导入的模块。请注意,ESM 中的所有静态导入总是提升的,因此将其放在静态导入之前不会强制它在导入之前被调用:
vi.doMock('./increment.js'); // 这将在 import 语句 _之后_ 被调用
import { increment } from './increment.js';
export function increment(number) {
return number + 1;
}
import { beforeEach, test } from 'vitest';
import { increment } from './increment.js';
// 模块未被模拟,因为 vi.doMock 尚未调用
increment(1) === 2;
let mockedIncrement = 100;
beforeEach(() => {
// 你可以在工厂函数内部访问变量
vi.doMock('./increment.js', () => ({ increment: () => ++mockedIncrement }));
});
test('导入下一个模块会导入模拟的模块', async () => {
// 原始导入未被模拟,因为 vi.doMock 在导入之后才进行评估
expect(increment(1)).toBe(2);
const { increment: mockedIncrement } = await import('./increment.js');
// 新的动态导入返回被模拟的模块
expect(mockedIncrement(1)).toBe(101);
expect(mockedIncrement(1)).toBe(102);
expect(mockedIncrement(1)).toBe(103);
});
vi.mocked
- 类型:
<T>(obj: T, deep?: boolean) => MaybeMockedDeep<T>
- 类型:
<T>(obj: T, options?: { partial?: boolean; deep?: boolean }) => MaybePartiallyMockedDeep<T>
TypeScript 类型辅助工具。仅返回传入的对象。
当 partial
为 true
时,它将期望 Partial<T>
作为返回值。默认情况下,这只会让 TypeScript 认为第一层的值被模拟了。你可以将 { deep: true }
作为第二个参数传递,以告诉 TypeScript 整个对象都被模拟了(如果它确实如此)。
export function add(x: number, y: number): number {
return x + y;
}
export function fetchSomething(): Promise<Response> {
return fetch('https://vitest.dev/');
}
import * as example from './example';
vi.mock('./example');
test('1 + 1 等于 10', async () => {
vi.mocked(example.add).mockReturnValue(10);
expect(example.add(1, 1)).toBe(10);
});
test('只用部分正确类型模拟返回值', async () => {
vi.mocked(example.fetchSomething).mockResolvedValue(new Response('hello'));
vi.mocked(example.fetchSomething, { partial: true }).mockResolvedValue({
ok: false,
});
// vi.mocked(example.someFn).mockResolvedValue({ ok: false }) // 这会导致类型错误
});
vi.importActual
- 类型:
<T>(path: string) => Promise<T>
导入模块,绕过所有是否需要模拟的检查。如果你想部分模拟模块,这会非常有用。
vi.mock('./example.js', async () => {
const originalModule = await vi.importActual('./example.js');
return { ...originalModule, get: vi.fn() };
});
vi.importMock
- 类型:
<T>(path: string) => Promise<MaybeMockedDeep<T>>
导入一个模块,其所有属性(包括嵌套属性)都已被模拟。遵循与 vi.mock
相同的规则。有关应用的规则,请参阅算法。
vi.unmock
- 类型:
(path: string | Promise<Module>) => void
从模拟模块注册表中移除模块。所有导入操作都将返回原始模块,即使它之前已被模拟。此调用会被提升到文件顶部,因此它只会取消对在 setupFiles
中定义的模块的模拟,例如。
vi.doUnmock
- 类型:
(path: string | Promise<Module>) => void
与 vi.unmock
相同,但不会被提升至文件顶部。该模块的下一次导入将导入原始模块而非模拟模块。这不会取消对之前已导入模块的模拟。
export function increment(number) {
return number + 1;
}
import { increment } from './increment.js';
// increment 已经被模拟,因为 vi.mock 已被提升
increment(1) === 100;
// 这被提升了,并且工厂函数在第 1 行的 import 之前被调用
vi.mock('./increment.js', () => ({ increment: () => 100 }));
// 所有调用都已被模拟,并且 `increment` 总是返回 100
increment(1) === 100;
increment(30) === 100;
// 这没有被提升,所以其他 import 将返回未模拟的模块
vi.doUnmock('./increment.js');
// 这仍然返回 100,因为 `vi.doUnmock` 不会重新评估该模块
increment(1) === 100;
increment(30) === 100;
// 下一个 import 未被模拟,现在 `increment` 是返回 count + 1 的原始函数
const { increment: unmockedIncrement } = await import('./increment.js');
unmockedIncrement(1) === 2;
unmockedIncrement(30) === 31;
vi.resetModules
- 类型:
() => Vitest
通过清除所有模块的缓存来重置模块注册。这允许模块在重新导入时被重新评估。顶层导入无法被重新评估。这对于隔离测试之间存在本地状态冲突的模块可能很有用。
import { vi } from 'vitest';
import { data } from './data.js'; // 不会在 beforeEach 测试中被重新评估
beforeEach(() => {
vi.resetModules();
});
test('改变状态', async () => {
const mod = await import('./some/path.js'); // 将被重新评估
mod.changeLocalState('new value');
expect(mod.getLocalState()).toBe('new value');
});
test('模块具有旧状态', async () => {
const mod = await import('./some/path.js'); // 将被重新评估
expect(mod.getLocalState()).toBe('old value');
});
WARNING
不重置模拟注册。要清除模拟注册,请使用 vi.unmock
或 vi.doUnmock
。
vi.dynamicImportSettled
等待所有导入加载完毕。如果你有一个同步调用,它开始导入一个无法以其他方式等待的模块,这会非常有用。
import { expect, test } from 'vitest';
// 无法跟踪导入,因为未返回 Promise
function renderComponent() {
import('./component.js').then(({ render }) => {
render();
});
}
test('操作已解决', async () => {
renderComponent();
await vi.dynamicImportSettled();
expect(document.querySelector('.component')).not.toBeNull();
});
TIP
如果在动态导入期间启动了另一个动态导入,此方法将等待所有导入都完成解析。
此方法还将在导入解析后等待下一个 setTimeout
计时周期,因此所有同步操作都应在该时间点完成。
模拟函数和对象
本节介绍如何使用模拟方法以及替换环境变量和全局变量。
vi.fn
- 类型:
(fn?: Function) => Mock
创建一个函数的 spy,也可以在没有函数的情况下初始化。每次函数被调用时,它都会存储其调用参数、返回值和实例。此外,你可以使用方法来控制其行为。 如果没有给定函数,模拟函数在调用时将返回 undefined
。
const getApples = vi.fn(() => 0);
getApples();
expect(getApples).toHaveBeenCalled();
expect(getApples).toHaveReturnedWith(0);
getApples.mockReturnValueOnce(5);
const res = getApples();
expect(res).toBe(5);
expect(getApples).toHaveNthReturnedWith(2, 5);
vi.mockObject 3.2.0+
- 类型:
<T>(value: T) => MaybeMockedDeep<T>
以与 vi.mock()
模拟模块导出相同的方式,深度模拟指定对象的属性和方法。详情请参阅自动模拟。
const original = {
simple: () => 'value',
nested: {
method: () => 'real',
},
prop: 'foo',
};
const mocked = vi.mockObject(original);
expect(mocked.simple()).toBe(undefined);
expect(mocked.nested.method()).toBe(undefined);
expect(mocked.prop).toBe('foo');
mocked.simple.mockReturnValue('mocked');
mocked.nested.method.mockReturnValue('mocked nested');
expect(mocked.simple()).toBe('mocked');
expect(mocked.nested.method()).toBe('mocked nested');
vi.isMockFunction
- 类型:
(fn: Function) => boolean
检查给定参数是否是模拟函数。如果你使用 TypeScript,它还会收窄其类型。
vi.clearAllMocks
对所有 spy 调用 .mockClear()
。 这将清除模拟函数的调用历史,而不影响模拟实现。
vi.resetAllMocks
对所有 spy 调用 .mockReset()
。 这将清除模拟函数的调用历史,并将每个模拟的实现重置为其原始实现。
vi.restoreAllMocks
对所有 spy 调用 .mockRestore()
。 这将清除模拟函数的调用历史,恢复所有原始模拟实现,并恢复被 spy 对象的原始描述符。
vi.spyOn
- 类型:
<T, K extends keyof T>(object: T, method: K, accessType?: 'get' | 'set') => MockInstance
在对象的某个方法或 getter/setter 上创建一个 spy,类似于 vi.fn()
。此方法返回一个模拟函数。
let apples = 0;
const cart = {
getApples: () => 42,
};
const spy = vi.spyOn(cart, 'getApples').mockImplementation(() => apples);
apples = 1;
expect(cart.getApples()).toBe(1);
expect(spy).toHaveBeenCalled();
expect(spy).toHaveReturnedWith(1);
TIP
在支持显式资源管理的环境中,你可以使用 using
而不是 const
,以便在包含块退出时自动对任何模拟函数调用 mockRestore
。这对于被侦听的方法特别有用:
it('调用 console.log', () => {
using spy = vi.spyOn(console, 'log').mockImplementation(() => {})
debug('message')
expect(spy).toHaveBeenCalled()
})
// console.log 在这里被恢复
TIP
你可以在 afterEach
中调用 vi.restoreAllMocks
(或启用 test.restoreMocks
)以将所有方法恢复到其原始实现。这将恢复原始的对象描述符,因此将无法更改方法的实现:
const cart = {
getApples: () => 42,
};
const spy = vi.spyOn(cart, 'getApples').mockReturnValue(10);
console.log(cart.getApples()); // 10
vi.restoreAllMocks();
console.log(cart.getApples()); // 42
spy.mockReturnValue(10);
console.log(cart.getApples()); // 仍然是 42!
TIP
在 浏览器模式 中无法侦听导出的方法。相反,你可以通过调用 vi.mock("./file-path.js", { spy: true })
来侦听每个导出的方法。这将模拟每个导出,但保持其实现不变,允许你断言方法是否被正确调用。
import { calculator } from './src/calculator.ts';
vi.mock('./src/calculator.ts', { spy: true });
calculator(1, 2);
expect(calculator).toHaveBeenCalledWith(1, 2);
expect(calculator).toHaveReturned(3);
虽然在 jsdom
或其他 Node.js 环境中可以侦听导出,但这在未来可能会改变。
vi.stubEnv
- 类型:
<T extends string>(name: T, value: T extends "PROD" | "DEV" | "SSR" ? boolean : string | undefined) => Vitest
更改 process.env
和 import.meta.env
中的环境变量值。你可以通过调用 vi.unstubAllEnvs
恢复其原始值。
import { vi } from 'vitest';
// 在调用 "vi.stubEnv" 之前,`process.env.NODE_ENV` 和 `import.meta.env.NODE_ENV`
// 都是 "development"
vi.stubEnv('NODE_ENV', 'production');
process.env.NODE_ENV === 'production';
import.meta.env.NODE_ENV === 'production';
vi.stubEnv('NODE_ENV', undefined);
process.env.NODE_ENV === undefined;
import.meta.env.NODE_ENV === undefined;
// 不会改变其他环境变量
import.meta.env.MODE === 'development';
TIP
你也可以通过简单地赋值来改变值,但将无法使用 vi.unstubAllEnvs
来恢复之前的值:
import.meta.env.MODE = 'test';
vi.unstubAllEnvs
- 类型:
() => Vitest
恢复所有通过 vi.stubEnv
更改的 import.meta.env
和 process.env
变量值。首次调用时,Vitest 会记住原始值并将其保存,直到再次调用 unstubAllEnvs
。
import { vi } from 'vitest';
// 在调用 stubEnv 之前,`process.env.NODE_ENV` 和 `import.meta.env.NODE_ENV`
// 都是 "development"
vi.stubEnv('NODE_ENV', 'production');
process.env.NODE_ENV === 'production';
import.meta.env.NODE_ENV === 'production';
vi.stubEnv('NODE_ENV', 'staging');
process.env.NODE_ENV === 'staging';
import.meta.env.NODE_ENV === 'staging';
vi.unstubAllEnvs();
// 恢复到第一次 "stubEnv" 调用之前保存的值
process.env.NODE_ENV === 'development';
import.meta.env.NODE_ENV === 'development';
vi.stubGlobal
- 类型:
(name: string | number | symbol, value: unknown) => Vitest
更改全局变量的数值。你可以通过调用 vi.unstubAllGlobals
恢复其原始数值。
import { vi } from 'vitest';
// 在调用 stubGlobal 之前,`innerWidth` 的值为 "0"
vi.stubGlobal('innerWidth', 100);
innerWidth === 100;
globalThis.innerWidth === 100;
// 如果你正在使用 jsdom 或 happy-dom
window.innerWidth === 100;
TIP
你也可以通过简单地将其赋值给 globalThis
或 window
(如果你正在使用 jsdom
或 happy-dom
环境)来更改值,但将无法使用 vi.unstubAllGlobals
来恢复原始值:
globalThis.innerWidth = 100;
// 如果你正在使用 jsdom 或 happy-dom
window.innerWidth = 100;
vi.unstubAllGlobals
- 类型:
() => Vitest
恢复所有通过 vi.stubGlobal
更改的 globalThis
/global
(以及 window
/top
/self
/parent
,如果你正在使用 jsdom
或 happy-dom
环境)上的全局变量值。首次调用时,Vitest 会记住原始值并将其保存,直到再次调用 unstubAllGlobals
。
import { vi } from 'vitest';
const Mock = vi.fn();
// 在调用 "stubGlobal" 之前,IntersectionObserver 的值为 "undefined"
vi.stubGlobal('IntersectionObserver', Mock);
IntersectionObserver === Mock;
global.IntersectionObserver === Mock;
globalThis.IntersectionObserver === Mock;
// 如果你正在使用 jsdom 或 happy-dom
window.IntersectionObserver === Mock;
vi.unstubAllGlobals();
globalThis.IntersectionObserver === undefined;
'IntersectionObserver' in globalThis === false;
// 抛出 ReferenceError,因为它尚未定义
IntersectionObserver === undefined;
模拟计时器
本节介绍如何使用模拟计时器。
vi.advanceTimersByTime
- 类型:
(ms: number) => Vitest
此方法将触发每个已启动的计时器,直到经过指定毫秒数或队列为空(以先发生者为准)。
let i = 0;
setInterval(() => console.log(++i), 50);
vi.advanceTimersByTime(150);
// log: 1
// log: 2
// log: 3
vi.advanceTimersByTimeAsync
- 类型:
(ms: number) => Promise<Vitest>
此方法将触发每个已启动的计时器,直到经过指定毫秒数或队列为空(以先发生者为准)。这将包括异步设置的计时器。
let i = 0;
setInterval(() => Promise.resolve().then(() => console.log(++i)), 50);
await vi.advanceTimersByTimeAsync(150);
// log: 1
// log: 2
// log: 3
vi.advanceTimersToNextTimer
- 类型:
() => Vitest
将触发下一个可用的计时器。这对于在每次计时器调用之间进行断言很有用。你可以链式调用它来手动管理计时器。
let i = 0;
setInterval(() => console.log(++i), 50);
vi.advanceTimersToNextTimer() // log: 1
.advanceTimersToNextTimer() // log: 2
.advanceTimersToNextTimer(); // log: 3
vi.advanceTimersToNextTimerAsync
- 类型:
() => Promise<Vitest>
将触发下一个可用的计时器,如果它是异步设置的,则等待其解决。这对于在每次计时器调用之间进行断言很有用。
let i = 0;
setInterval(() => Promise.resolve().then(() => console.log(++i)), 50);
await vi.advanceTimersToNextTimerAsync(); // log: 1
expect(console.log).toHaveBeenCalledWith(1);
await vi.advanceTimersToNextTimerAsync(); // log: 2
await vi.advanceTimersToNextTimerAsync(); // log: 3
vi.advanceTimersToNextFrame 2.1.0+
- 类型:
() => Vitest
类似于 vi.advanceTimersByTime
,但会根据执行当前使用 requestAnimationFrame
调度的回调所需的毫秒数来推进计时器。
let frameRendered = false;
requestAnimationFrame(() => {
frameRendered = true;
});
vi.advanceTimersToNextFrame();
expect(frameRendered).toBe(true);
vi.getTimerCount
- 类型:
() => number
获取待执行计时器的数量。
vi.clearAllTimers
移除所有已计划运行的计时器。这些计时器将永远不会被执行。
vi.getMockedSystemTime
- 类型:
() => Date | null
返回被模拟的当前日期。如果日期未被模拟,该方法将返回 null
。
vi.getRealSystemTime
- 类型:
() => number
当使用 vi.useFakeTimers
时,Date.now
调用会被模拟。如果你需要获取真实的毫秒时间戳,可以调用此函数。
vi.runAllTicks
- 类型:
() => Vitest
执行所有由 process.nextTick
排队的微任务。这也会运行所有由自身调度的微任务。
vi.runAllTimers
- 类型:
() => Vitest
此方法将触发每个已启动的计时器,直到计时器队列为空。这意味着在 runAllTimers
期间调用的每个计时器都将被执行。如果你有一个无限循环的间隔计时器,它将在 10,000 次尝试后抛出(可以通过 fakeTimers.loopLimit
配置)。
let i = 0;
setTimeout(() => console.log(++i));
const interval = setInterval(() => {
console.log(++i);
if (i === 3) {
clearInterval(interval);
}
}, 50);
vi.runAllTimers();
// log: 1
// log: 2
// log: 3
vi.runAllTimersAsync
- 类型:
() => Promise<Vitest>
此方法将异步触发每个已启动的计时器,直到计时器队列为空。这意味着在 runAllTimersAsync
期间调用的每个计时器都将被执行,即使是异步计时器。如果你有一个无限间隔, 它将在 10,000 次尝试后抛出(可以通过 fakeTimers.loopLimit
配置)。
setTimeout(async () => {
console.log(await Promise.resolve('result'));
}, 100);
await vi.runAllTimersAsync();
// log: result
vi.runOnlyPendingTimers
- 类型:
() => Vitest
此方法将触发在 vi.useFakeTimers
调用之后启动的每个计时器。它不会触发在其自身调用期间启动的任何计时器。
let i = 0;
setInterval(() => console.log(++i), 50);
vi.runOnlyPendingTimers();
// log: 1
vi.runOnlyPendingTimersAsync
- 类型:
() => Promise<Vitest>
此方法将异步触发在 vi.useFakeTimers
调用之后启动的每个计时器,甚至是异步计时器。它不会触发在其自身调用期间启动的任何计时器。
setTimeout(() => {
console.log(1);
}, 100);
setTimeout(() => {
Promise.resolve().then(() => {
console.log(2);
setInterval(() => {
console.log(3);
}, 40);
});
}, 10);
await vi.runOnlyPendingTimersAsync();
// log: 2
// log: 3
// log: 3
// log: 1
vi.setSystemTime
- 类型:
(date: string | number | Date) => void
如果启用了模拟计时器,此方法会模拟用户更改系统时钟(将影响与日期相关的 API,如 hrtime
、performance.now
或 new Date()
)——但是,它不会触发任何计时器。如果未启用模拟计时器,此方法只会模拟 Date.*
调用。
如果你需要测试任何依赖于当前日期的内容,例如代码中的 Luxon 调用,这会非常有用。
接受与 Date
相同的字符串和数字参数。
const date = new Date(1998, 11, 19);
vi.useFakeTimers();
vi.setSystemTime(date);
expect(Date.now()).toBe(date.valueOf());
vi.useRealTimers();
vi.useFakeTimers
- 类型:
(config?: FakeTimerInstallOpts) => Vitest
要启用模拟计时器,需要调用此方法。它将拦截所有后续对计时器的调用(例如 setTimeout
、setInterval
、clearTimeout
、clearInterval
、setImmediate
、clearImmediate
和 Date
),直到调用 vi.useRealTimers()
。
当 Vitest 在 node:child_process
中使用 --pool=forks
运行时,不支持模拟 nextTick
。NodeJS 在 node:child_process
内部使用 process.nextTick
,并在模拟时可能导致程序挂起。当 Vitest 使用 --pool=threads
运行时,支持模拟 nextTick
。
该实现内部基于 @sinonjs/fake-timers
。
TIP
vi.useFakeTimers()
不会自动模拟 process.nextTick
和 queueMicrotask
。 但你可以通过在 toFake
参数中指定选项来启用它:vi.useFakeTimers({ toFake: ['nextTick', 'queueMicrotask'] })
。
vi.isFakeTimers
- 类型:
() => boolean
如果启用了模拟计时器,则返回 true
。
vi.useRealTimers
- 类型:
() => Vitest
当计时器执行完毕时,你可以调用此方法将模拟计时器恢复到其原始实现。所有之前已计划的计时器都将被丢弃。
杂项
Vitest 提供的一组实用辅助函数。
vi.waitFor
- 类型:
<T>(callback: WaitForCallback<T>, options?: number | WaitForOptions) => Promise<T>
等待回调函数成功执行。如果回调抛出错误或返回一个被拒绝的 Promise,它将继续等待直到成功或超时。
如果 options 设置为数字,则效果等同于设置 { timeout: options }
。
这在需要等待异步操作完成时非常有用,例如,当你启动服务器并需要等待其启动时。
import { expect, test, vi } from 'vitest';
import { createServer } from './server.js';
test('服务器启动成功', async () => {
const server = createServer();
await vi.waitFor(
() => {
if (!server.isReady) {
throw new Error('服务器未启动');
}
console.log('服务器已启动');
},
{
timeout: 500, // 默认值是 1000
interval: 20, // 默认值是 50
}
);
expect(server.isReady).toBe(true);
});
它也适用于异步回调
// @vitest-environment jsdom
import { expect, test, vi } from 'vitest';
import { getDOMElementAsync, populateDOMAsync } from './dom.js';
test('元素存在于 DOM 中', async () => {
// 开始填充 DOM
populateDOMAsync();
const element = await vi.waitFor(
async () => {
// 持续尝试获取元素直到其存在
const element = (await getDOMElementAsync()) as HTMLElement | null;
expect(element).toBeTruthy();
expect(element.dataset.initialized).toBeTruthy();
return element;
},
{
timeout: 500, // 默认值是 1000
interval: 20, // 默认值是 50
}
);
expect(element).toBeInstanceOf(HTMLElement);
});
如果使用了 vi.useFakeTimers
,vi.waitFor
会在每次检查回调中自动调用 vi.advanceTimersByTime(interval)
。
vi.waitUntil
- 类型:
<T>(callback: WaitUntilCallback<T>, options?: number | WaitUntilOptions) => Promise<T>
这类似于 vi.waitFor
,但如果回调抛出任何错误,执行会立即中断并返回错误消息。如果回调返回假值,下一次检查将继续,直到返回真值。当你需要等待某个东西存在才能进行下一步时,这会非常有用。
请看下面的例子。我们可以使用 vi.waitUntil
等待元素出现在页面上,然后我们可以对元素做一些事情。
import { expect, test, vi } from 'vitest';
test('元素渲染正确', async () => {
const element = await vi.waitUntil(() => document.querySelector('.element'), {
timeout: 500, // 默认值是 1000
interval: 20, // 默认值是 50
});
// 对元素做一些事情
expect(element.querySelector('.element-child')).toBeTruthy();
});
vi.hoisted
- 类型:
<T>(factory: () => T) => T
ES 模块中的所有静态 import
语句都会被提升到文件顶部,因此在导入之前定义的任何代码实际上都会在导入评估之后执行。
然而,在导入模块之前执行一些副作用(如模拟日期)可能很有用。
为了绕过这个限制,你可以将静态导入重写为动态导入,如下所示:
callFunctionWithSideEffect()
- import { value } from './some/module.js'
+ const { value } = await import('./some/module.js')
运行 vitest
时,可以使用 vi.hoisted
方法自动完成此操作。在底层,Vitest 会将静态导入转换为动态导入,并保留实时绑定。
- callFunctionWithSideEffect()
import { value } from './some/module.js'
+ vi.hoisted(() => callFunctionWithSideEffect())
无法使用导入
在导入之前运行代码意味着无法访问导入的变量,因为它们尚未定义:
import { value } from './some/module.js';
vi.hoisted(() => { value }); // 抛出错误
此代码将引发错误:
Cannot access '__vi_import_0__' before initialization
如果需要在 vi.hoisted
内部访问另一个模块的变量,请使用动态导入:
await vi.hoisted(async () => {
const { value } = await import('./some/module.js');
});
然而,不建议在 vi.hoisted
内部导入任何东西,因为导入已经被提升了——如果你需要在测试运行之前执行某些操作,只需在导入的模块本身中执行即可。
此方法返回从工厂函数返回的值。你可以在 vi.mock
工厂函数中使用该值,如果你需要轻松访问本地定义的变量:
import { expect, vi } from 'vitest';
import { originalMethod } from './path/to/module.js';
const { mockedMethod } = vi.hoisted(() => {
return { mockedMethod: vi.fn() };
});
vi.mock('./path/to/module.js', () => {
return { originalMethod: mockedMethod };
});
mockedMethod.mockReturnValue(100);
expect(originalMethod()).toBe(100);
请注意,即使环境不支持顶层 await,此方法也可以异步调用:
const json = await vi.hoisted(async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
return response.json();
});
vi.setConfig
- 类型:
RuntimeConfig
更新当前测试文件的配置。此方法仅支持影响当前测试文件的配置选项:
vi.setConfig({
allowOnly: true,
testTimeout: 10_000,
hookTimeout: 10_000,
clearMocks: true,
restoreMocks: true,
fakeTimers: {
now: new Date(2021, 11, 19),
// 支持整个对象
},
maxConcurrency: 10,
sequence: {
hooks: 'stack',
// 仅支持 "sequence.hooks"
},
});
vi.resetConfig
- 类型:
RuntimeConfig
如果之前调用过 vi.setConfig
,这将把配置重置为原始状态。