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
调用会被提升(hoisted),因此你在哪里调用它并不重要,它总会在所有导入之前执行。如果你需要引用作用域外的变量,可以在 vi.hoisted
中定义它们,并在 vi.mock
中引用。
WARNING
vi.mock
仅适用于使用 import
关键字导入的模块,不适用于 require
。
为了提升 vi.mock
,Vitest 会静态分析你的文件。这意味着不能使用未直接从 vitest
包导入的 vi
(例如,从某个实用工具文件导入)。请使用从 vitest
导入的 vi
来使用 vi.mock
,或者启用 globals
配置选项。
Vitest 不会模拟在 setup 文件 中导入的模块,因为它们在测试文件运行时已被缓存。你可以在 vi.hoisted
中调用 vi.resetModules()
以在运行测试文件之前清除所有模块缓存。
如果定义了 factory
函数,所有导入都将返回其结果。Vitest 只调用一次 factory 并缓存所有后续导入的结果,直到调用 vi.unmock
或 vi.doUnmock
为止。
与 jest
不同,factory 可以是异步的。你可以使用 vi.importActual
或一个助手,将 factory 作为第一个参数传入,并在 factory 中获取原始模块。
自 Vitest 2.1 起,你还可以提供一个带有 spy
属性的对象,而不是 factory 函数。如果 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
会自动继承类型。使用此签名还将强制 factory 返回类型与原始模块兼容(保持导出可选)。
// @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 仍然操作字符串而不是模块对象。
但是,如果你在使用 TypeScript 并配置了 tsconfig.json
中的 paths
别名,编译器将无法正确解析导入类型。 为了使其工作,请确保将所有别名导入替换为其对应的相对路径。 例如,使用 import('./path/to/module.js')
而不是 import('@/module')
。
WARNING
vi.mock
会被提升(换句话说,移动)到文件顶部。这意味着无论你在哪里编写它(无论是在 beforeEach
还是 test
中),它实际上都会在那之前被调用。
这也意味着你不能在 factory 中使用在 factory 外部定义的任何变量。
如果你需要在 factory 中使用变量,请尝试 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
如果你正在模拟一个带有默认导出的模块,你需要在返回的 factory 函数对象中提供一个 default
键。这是一个 ES 模块特定的注意事项;因此,jest
文档可能有所不同,因为 jest
使用 CommonJS 模块。例如,
vi.mock('./path/to/module.js', () => {
return {
default: { myDefaultKey: vi.fn() },
namedExport: vi.fn(),
// 等等...
};
});
如果被模拟的文件旁边有一个 __mocks__
文件夹,并且没有提供 factory,Vitest 将尝试在 __mocks__
子文件夹中查找同名文件并将其用作实际模块。如果你正在模拟一个依赖项,Vitest 将尝试在项目 root(默认为 process.cwd()
)中查找 __mocks__
文件夹。你可以通过 deps.moduleDirectories
配置选项告诉 Vitest 依赖项的位置。
例如,你有以下文件结构:
- __mocks__
- axios.js
- src
__mocks__
- increment.js
- increment.js
- tests
- increment.test.js
如果你在测试文件中调用 vi.mock
而没有提供 factory 或选项,它将在 __mocks__
文件夹中找到一个文件用作模块:
// increment.test.js
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__
文件夹或未提供 factory,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';
// ./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(() => {
// 你可以在 factory 中访问变量
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 整个对象都被模拟了(如果它确实如此)。
// example.ts
export function add(x: number, y: number): number {
return x + y;
}
export function fetchSomething(): Promise<Response> {
return fetch('https://vitest.dev/');
}
// example.test.ts
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 axios = await vi.importActual('./example.js');
return { ...axios, 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
相同,但不会被提升到文件顶部。模块的下一个导入将导入原始模块而不是模拟。这不会取消模拟之前导入的模块。
// ./increment.js
export function increment(number) {
return number + 1;
}
import { increment } from './increment.js';
// increment 已经被模拟,因为 vi.mock 被提升了
increment(1) === 100;
// 这被提升了,并且 factory 在第 1 行的 import 之前被调用
vi.mock('./increment.js', () => ({ increment: () => 100 }));
// 所有调用都被模拟了,并且 `increment` 总是返回 100
increment(1) === 100;
increment(30) === 100;
// 这没有被提升,所以其他导入将返回未被模拟的模块
vi.doUnmock('./increment.js');
// 这仍然返回 100,因为 `vi.doUnmock` 不会重新评估模块
increment(1) === 100;
increment(30) === 100;
// 下一个导入未被模拟,现在 `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,尽管可以在没有函数的情况下初始化。每次调用函数时,它都会存储其调用参数、返回值和实例。此外,你可以使用方法来操作其行为。 如果没有提供函数,调用 mock 时将返回 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.isMockFunction
- 类型:
(fn: Function) => boolean
检查给定的参数是否是模拟函数。如果你使用 TypeScript,它还会缩小其类型。
vi.clearAllMocks
将对所有 spies 调用 .mockClear()
。这将清除模拟历史记录,但不会将其实现重置为默认实现。
vi.resetAllMocks
将对所有 spies 调用 .mockReset()
。这将清除模拟历史记录并将其实现重置为空函数(将返回 undefined
)。
vi.restoreAllMocks
将对所有 spies 调用 .mockRestore()
。这将清除模拟历史记录并将其实现重置为原始实现。
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
你可以在 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
在 浏览器模式 中无法 spy 导出的方法。相反,你可以通过调用 vi.mock("./file-path.js", { spy: true })
来 spy 每个导出的方法。这将模拟每个导出,但保持其实现不变,允许你断言方法是否被正确调用。
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 环境中可以 spy 导出,但这将来可能会改变。
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;
// 不改变其他 envs
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
返回使用 setSystemTime
设置的模拟当前日期。如果日期未被模拟,该方法将返回 null
。
vi.getRealSystemTime
- 类型:
() => number
使用 vi.useFakeTimers
时,Date.now
调用会被模拟。如果你需要获取真实的毫秒时间,可以调用此函数。
vi.runAllTicks
- 类型:
() => Vitest
调用所有由 process.nextTick
排队的微任务。这也会运行所有由它们自己调度的微任务。
vi.runAllTimers
- 类型:
() => Vitest
此方法将调用每个已启动的计时器,直到计时器队列为空。这意味着在 runAllTimers
期间调用的每个计时器都将被触发。如果你有一个无限间隔,它将在尝试 10000 次后抛出错误(可以使用 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
期间调用的每个计时器都将被触发,即使是异步计时器。如果你有一个无限间隔,它将在尝试 10000 次后抛出错误(可以使用 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()
。
当使用 --pool=forks
在 node:child_process
中运行 Vitest 时,不支持模拟 nextTick
。NodeJS 在 node:child_process
内部使用 process.nextTick
,模拟它会导致挂起。当使用 --pool=threads
运行 Vitest 时,支持模拟 nextTick
。
实现内部基于 @sinonjs/fake-timers
。
TIP
vi.useFakeTimers()
不会自动模拟 process.nextTick
。 但你可以通过在 toFake
参数中指定选项来启用它:vi.useFakeTimers({ toFake: ['nextTick'] })
。
vi.isFakeTimers
- 类型:
() => boolean
如果启用了伪造计时器,则返回 true
。
vi.useRealTimers
- 类型:
() => Vitest
当计时器用尽时,你可以调用此方法将模拟计时器恢复到其原始实现。所有之前计划的计时器都将被丢弃。
其他
Vitest 提供的一组有用的辅助函数。
vi.waitFor
- 类型:
<T>(callback: WaitForCallback<T>, options?: number | WaitForOptions) => Promise<T>
等待回调成功执行。如果回调抛出错误或返回被拒绝的 Promise,它将继续等待直到成功或超时。
用于等待异步操作完成,例如,当你启动服务器并需要等待它启动时。
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
语句都会被提升(hoisted)到文件顶部,因此在导入之前定义的任何代码实际上都会在导入评估之后执行。
然而,在导入模块之前调用一些副作用(例如模拟日期)可能很有用。
为解决此限制,你可以将静态导入重写为动态导入,如下所示:
callFunctionWithSideEffect()
- import { value } from './some/module.js'
+ const { value } = await import('./some/module.js')
在 Vitest 中,你可以使用 vi.hoisted
方法自动完成此操作。
- callFunctionWithSideEffect()
import { value } from './some/module.js'
+ vi.hoisted(() => callFunctionWithSideEffect())
此方法返回 factory 返回的值。如果你需要方便访问局部定义的变量,可以在 vi.mock
factory 中使用该值:
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 promised = 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
,这将把配置重置为原始状态。