Vi
Vitest 通过 vi
辅助工具提供实用函数来帮助你。你可以全局访问它(当 globals 配置 启用 时),或者从 vitest
导入:
import { vi } from 'vitest';
vi.advanceTimersByTime
类型:
(ms: number) => Vitest
与
runAllTimers
功能类似,但会在经过指定的毫秒数后结束。例如,以下代码会记录1, 2, 3
,且不会抛出错误:tslet i = 0; setInterval(() => console.log(++i), 50); vi.advanceTimersByTime(150);
vi.advanceTimersByTimeAsync
类型:
(ms: number) => Promise<Vitest>
与
runAllTimersAsync
功能类似,但会在经过指定的毫秒数后结束。此方法会处理异步设置的定时器。例如,以下代码会记录1, 2, 3
,且不会抛出错误:tslet i = 0; setInterval(() => Promise.resolve().then(() => console.log(++i)), 50); await vi.advanceTimersByTimeAsync(150);
vi.advanceTimersToNextTimer
类型:
() => Vitest
执行下一个可用的定时器。可用于在定时器调用之间进行断言。你可以链式调用此方法来手动管理定时器。
tslet i = 0; setInterval(() => console.log(++i), 50); vi.advanceTimersToNextTimer() // 记录 1 .advanceTimersToNextTimer() // 记录 2 .advanceTimersToNextTimer(); // 记录 3
vi.advanceTimersToNextTimerAsync
类型:
() => Promise<Vitest>
执行下一个可用的定时器,即使是异步设置的定时器也会被处理。可用于在定时器调用之间进行断言。你可以链式调用此方法来手动管理定时器。
tslet i = 0; setInterval(() => Promise.resolve().then(() => console.log(++i)), 50); vi.advanceTimersToNextTimerAsync() // 记录 1 .advanceTimersToNextTimerAsync() // 记录 2 .advanceTimersToNextTimerAsync(); // 记录 3
vi.getTimerCount
类型:
() => number
获取待执行的定时器数量。
vi.clearAllMocks
对所有间谍调用 .mockClear()
。这将清除模拟历史记录,但不会将实现重置为默认实现。
vi.clearAllTimers
移除所有已计划但尚未执行的定时器。这些定时器将不会再运行。
vi.dynamicImportSettled
等待所有动态导入完成加载。如果存在一个同步调用,它会开始导入一个模块,而你又无法以其他方式等待其完成,那么此方法会非常有用。
vi.fn
类型:
(fn?: Function) => Mock
创建一个函数的间谍,也可以不使用函数进行初始化。每次函数被调用时,都会记录调用参数、返回值和实例。此外,你可以使用 方法 来修改其行为。 如果没有提供函数,则在调用时,模拟将返回
undefined
。tsconst 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.getMockedSystemTime
类型:
() => Date | null
返回通过
setSystemTime
设置的模拟当前日期。如果日期未被模拟,则返回null
。
vi.getRealSystemTime
类型:
() => number
当使用
vi.useFakeTimers
时,Date.now
的调用会被模拟。如果你需要获取真实的毫秒时间,可以调用此函数。
vi.hoisted
类型:
<T>(factory: () => T) => T
版本: 自 Vitest 0.31.0 起
ES 模块中所有静态
import
语句都会被提升到文件顶部,因此在导入语句之前定义的任何代码,实际上会在导入语句被执行之后执行。但是,在导入模块之前调用一些副作用(如模拟日期)可能很有用。
要解决这个问题,可以将静态导入重写为动态导入,如下所示:
diffcallFunctionWithSideEffect() - import { value } from './some/module.ts' + const { value } = await import('./some/module.ts')
在运行
vitest
时,可以使用vi.hoisted
方法自动执行此操作。diff- callFunctionWithSideEffect() import { value } from './some/module.ts' + vi.hoisted(() => callFunctionWithSideEffect())
此方法返回工厂函数返回的值。如果需要轻松访问本地定义的变量,可以在
vi.mock
工厂函数中使用该值:tsimport { 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);
vi.mock
类型:
(path: string, factory?: () => unknown) => void
将指定路径导入的所有模块替换为另一个模块。可以在路径中使用配置的 Vite 别名。
vi.mock
的调用会被提升,因此调用位置并不重要。它总是在所有导入之前执行。如果需要引用其范围之外的一些变量,你可以在vi.hoisted
中定义它们,并在vi.mock
中引用。WARNING
vi.mock
仅适用于通过import
关键字导入的模块。它不适用于require
。Vitest 会静态分析文件以提升
vi.mock
。这意味着不能使用未直接从vitest
包导入的vi
,例如从某些实用工具文件中导入的vi
。要解决此问题,请始终使用从vitest
导入的vi
来使用vi.mock
,或启用globals
配置选项。如果定义了
factory
,则所有导入将返回其结果。Vitest 只会调用工厂函数一次,并将结果缓存起来,供后续所有导入操作使用,直到调用vi.unmock
或vi.doUnmock
为止。与
jest
不同,工厂函数可以是异步的,因此可以在其中使用vi.importActual
或作为第一个参数接收的辅助函数来获取原始模块。tsvi.mock('./path/to/module.js', async importOriginal => { const mod = await importOriginal(); return { ...mod, // 替换一些导出 namedExport: vi.fn(), }; });
WARNING
vi.mock
会被提升(换句话说,移动)到 文件顶部。这意味着无论在哪里编写它(无论是在beforeEach
还是test
中),它实际上都会在之前被调用。这也意味着不能在工厂函数内部使用在外部定义的任何变量。
如果需要在工厂函数内部使用变量,请尝试
vi.doMock
。它的工作方式相同,但不会被提升。请注意,它只会模拟后续导入。如果在
vi.mock
之前声明,也可以引用在vi.hoisted
方法中定义的变量:tsimport { 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 模块。例如,tsvi.mock('./path/to/module.js', () => { return { default: { myDefaultKey: vi.fn() }, namedExport: vi.fn(), // 等等... }; });
如果有一个与你要模拟的文件并排的
__mocks__
文件夹,并且未提供工厂函数,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
而不提供工厂函数,它将找到__mocks__
文件夹中的文件作为模块。ts// 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__
文件夹或提供工厂函数,Vitest 将导入原始模块并自动模拟所有导出。有关应用的规则,请参见 算法。
vi.doMock
类型:
(path: string, factory?: () => unknown) => void
与
vi.mock
相同,但它不会被提升到文件顶部,因此可以在全局文件范围内引用变量。该模块的下一次 动态导入 将会被模拟。这不会模拟在此调用之前导入的模块。
// ./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>
。tsimport example from './example.js'; vi.mock('./example.js'); test('1+1 等于 2', async () => { vi.mocked(example.calc).mockRestore(); const res = example.calc(1, '+', 1); expect(res).toBe(2); });
vi.importActual
类型:
<T>(path: string) => Promise<T>
导入模块,并跳过所有关于是否需要进行模拟的检查。这在需要部分模拟模块时非常有用。
tsvi.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.resetAllMocks
将调用所有间谍的 .mockReset()
。这将清除模拟历史记录,并将其实现重置为空函数(返回 undefined
)。
vi.resetConfig
类型:
RuntimeConfig
如果之前调用了
vi.setConfig
,则将配置重置为原始状态。
vi.resetModules
类型:
() => Vitest
通过清除所有模块缓存来重置模块注册表。这允许在重新导入时重新评估模块。顶级导入无法被重新评估。这可能有助于隔离那些在不同测试之间存在本地状态冲突的模块。
tsimport { vi } from 'vitest'; import { data } from './data.js'; // 将不会在每个测试之前重新评估 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.restoreAllMocks
将调用所有间谍的 .mockRestore()
。这将清除模拟历史记录,并将其实现重置为原始实现。
vi.stubEnv
类型:
(name: string, value: string) => Vitest
版本: 自 Vitest 0.26.0 起
更改
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';
// 不会更改其他环境变量的值
import.meta.env.MODE === 'development';
TIP
也可以通过简单赋值来更改该值,但无法使用 vi.unstubAllEnvs
恢复先前的值:
import.meta.env.MODE = 'test';
vi.unstubAllEnvs
类型:
() => Vitest
版本: 自 Vitest 0.26.0 版本起
恢复所有被
vi.stubEnv
修改过的import.meta.env
和process.env
的值。首次调用vi.stubEnv
时,Vitest 会记住并存储原始值,直到再次调用vi.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
版本: 自 Vitest 0.26.0 版本起
恢复所有通过
vi.stubGlobal()
更改的globalThis
/global
(以及window
/top
/self
/parent
,如果使用jsdom
或happy-dom
环境) 上的全局值。首次调用vi.stubGlobal()
时,Vitest 会记住并存储原始值,直到再次调用vi.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.runAllTicks
类型:
() => Vitest
调用所有排队的微任务
process.nextTick
。 这也会运行所有由这些微任务自身调度的微任务。
vi.runAllTimers
类型:
() => Vitest
此方法会执行所有已启动的计时器,直到计时器队列为空。 这意味着在
vi.runAllTimers()
执行期间调用的所有计时器也会被触发。 如果存在无限间隔的计时器,它将在尝试 10000 次后抛出一个错误。 例如,以下代码会输出1, 2, 3
:tslet i = 0; setTimeout(() => console.log(++i)); const interval = setInterval(() => { console.log(++i); if (i === 3) clearInterval(interval); }, 50); vi.runAllTimers();
vi.runAllTimersAsync
类型:
() => Promise<Vitest>
此方法会异步执行所有已启动的计时器,直到计时器队列为空。 这意味着在
vi.runAllTimersAsync()
执行期间调用的每个计时器都将被触发,包括异步计时器。 如果存在无限间隔的计时器,它将在尝试 10000 次后抛出一个错误。 例如,以下代码会输出result
:tssetTimeout(async () => { console.log(await Promise.resolve('result')); }, 100); await vi.runAllTimersAsync();
vi.runOnlyPendingTimers
类型:
() => Vitest
此方法会执行在
vi.useFakeTimers()
调用之后启动的所有计时器。 它不会触发在其调用期间启动的任何计时器。 例如,以下代码只会输出1
:tslet i = 0; setInterval(() => console.log(++i), 50); vi.runOnlyPendingTimers();
vi.runOnlyPendingTimersAsync
类型:
() => Promise<Vitest>
此方法会异步执行在
vi.useFakeTimers()
调用之后启动的所有计时器,包括异步计时器。 它不会触发在其调用期间启动的任何计时器。 例如,以下代码会输出2, 3, 3, 1
:tssetTimeout(() => { console.log(1); }, 100); setTimeout(() => { Promise.resolve().then(() => { console.log(2); setInterval(() => { console.log(3); }, 40); }); }, 10); await vi.runOnlyPendingTimersAsync();
vi.setSystemTime
类型:
(date: string | number | Date) => void
将当前日期设置为指定的日期。 所有
Date
相关的调用都将返回此日期。如果需要测试依赖于当前日期的代码(例如,代码中使用了 luxon),这将非常有用。
tsconst date = new Date(1998, 11, 19); vi.useFakeTimers(); vi.setSystemTime(date); expect(Date.now()).toBe(date.valueOf()); vi.useRealTimers();
vi.setConfig
类型:
RuntimeConfig
更新当前测试文件的配置项。 您只能影响执行测试时使用的配置值。
vi.spyOn
类型:
<T, K extends keyof T>(object: T, method: K, accessType?: 'get' | 'set') => MockInstance
在对象的方法或 getter/setter 上创建一个 spy。
tslet apples = 0; const cart = { getApples: () => 13, }; const spy = vi.spyOn(cart, 'getApples').mockImplementation(() => apples); apples = 1; expect(cart.getApples()).toBe(1); expect(spy).toHaveBeenCalled(); expect(spy).toHaveReturnedWith(1);
vi.stubGlobal
类型:
(key: keyof globalThis & Window, value: any) => Vitest
将值设置为全局变量。 如果您正在使用
jsdom
或happy-dom
,也会将该值设置到window
对象上。请参阅 "模拟全局变量" 部分 了解更多信息。
vi.unmock
类型:
(path: string) => void
从模拟注册表中移除模块。 所有后续的导入调用都将返回原始模块,即使它之前被模拟过。 此调用会被提升到文件顶部,因此只会取消模拟在
setupFiles
中定义的模块。
vi.doUnmock
类型:
(path: string) => void
与
vi.unmock
相同,但不会被提升到文件顶部。 模块的下一次导入会导入原始模块,而不是模拟模块。 这不会影响先前已经导入的模块。
// ./increment.js
export function increment(number) {
return number + 1;
}
import { increment } from './increment.js';
// increment 已经被模拟,因为 vi.mock 被提升了
increment(1) === 100;
// 这会被提升,并在第 1 行导入之前调用工厂函数
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.useFakeTimers
类型:
() => Vitest
要启用计时器模拟,您需要调用此方法。 它会拦截所有后续的定时器调用(例如
setTimeout
、setInterval
、clearTimeout
、clearInterval
、nextTick
、setImmediate
、clearImmediate
和Date
),直到调用vi.useRealTimers()
。在
node:child_process
中使用--no-threads
运行 Vitest 时,不支持模拟nextTick
。 因为 NodeJS 在node:child_process
内部使用process.nextTick
,模拟后会导致程序挂起。 使用--threads
运行 Vitest 时,则支持模拟nextTick
。该实现内部基于
@sinonjs/fake-timers
。TIP
从
0.35.0
版本开始,vi.useFakeTimers()
不再自动模拟process.nextTick
。 如果需要模拟,可以在toFake
参数中指定:vi.useFakeTimers({ toFake: ['nextTick'] })
。
vi.isFakeTimers
类型:
() => boolean
版本: 自 Vitest 0.34.5 起
如果启用了 fake timers,则返回
true
。
vi.useRealTimers
类型:
() => Vitest
调用此方法可以将模拟定时器恢复为原始实现。 之前运行的所有定时器不受影响。
vi.waitFor
- 类型:
<T>(callback: WaitForCallback<T>, options?: number | WaitForOptions) => Promise<T>
- 版本: 自 Vitest 0.34.5 起
等待回调函数成功执行。 如果回调函数抛出错误或返回 rejected 状态的 Promise,vi.waitFor
将持续等待,直到成功或超时。
当需要等待某些异步操作完成时,此方法非常有用。 例如,在启动服务器后,需要等待服务器启动完成。
import { expect, test, vi } from 'vitest';
import { createServer } from './server.js';
test('Server started successfully', async () => {
const server = createServer();
await vi.waitFor(
() => {
if (!server.isReady) throw new Error('Server not started');
console.log('Server started');
},
{
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('Element exists in a 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>
- 版本: 自 Vitest 0.34.5 起
与 vi.waitFor
类似,但如果回调函数抛出任何错误,执行会立即中断并抛出错误。 如果回调函数返回一个 falsy 值,则会继续进行下一次检查,直到返回一个 truthy 值。 当需要等待某个条件成立后才能执行下一步操作时,此方法非常有用。
以下示例展示了如何使用 vi.waitUntil
等待页面元素出现,然后再执行后续操作。
import { expect, test, vi } from 'vitest';
test('Element render correctly', async () => {
const element = await vi.waitUntil(() => document.querySelector('.element'), {
timeout: 500, // 默认值为 1000
interval: 20, // 默认值为 50
});
// 对该元素执行某些操作
expect(element.querySelector('.element-child')).toBeTruthy();
});