Skip to content
Vitest 3
Main Navigation 指南 & API配置浏览器模式高级 API
3.2.0
2.1.9
1.6.1
0.34.6

简体中文

English
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

简体中文

English
繁體中文
Español
Français
Русский
Português – Brasil
Deutsch
日本語
한국어
Italiano
Polski
Türkçe
čeština
magyar

主题

Sidebar Navigation

简介

为什么选择 Vitest

快速入门

特性

配置 Vitest

API

测试 API 参考

模拟函数

Vi

expect

expectTypeOf

assert

assertType

指南

命令行界面

测试过滤

测试项目

报告器

覆盖率

快照

模拟

并行

类型测试

Vitest UI

源内测试

测试上下文

测试注解

测试环境

扩展匹配器

IDE 集成

调试

常见错误

迁移指南

迁移到 Vitest 3.0

从 Jest 迁移

性能

分析测试性能

性能优化

浏览器模式

高级 API

与其他测试运行器的比较

页面导航

Vi ​

Vitest 通过其 vi 辅助工具提供实用功能。你可以在全局范围内访问它(当 globals 配置 启用时),或者直接从 vitest 导入:

js
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 将照常自动模拟模块,但它不会覆盖导出的实现。如果你只是想断言导出的方法被另一个方法正确调用,这会非常有用。

ts
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 会自动继承类型。使用此签名还将强制工厂函数的返回类型与原始模块兼容(保持导出可选)。

ts
// @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 之前声明,你也可以引用它定义的变量:

ts
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 模块。例如,

ts
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__ 文件夹中查找文件以用作模块:

ts
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 中的所有静态导入总是提升的,因此将其放在静态导入之前不会强制它在导入之前被调用:

ts
vi.doMock('./increment.js'); // 这将在 import 语句 _之后_ 被调用

import { increment } from './increment.js';
ts
export function increment(number) {
  return number + 1;
}
ts
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 整个对象都被模拟了(如果它确实如此)。

ts
export function add(x: number, y: number): number {
  return x + y;
}

export function fetchSomething(): Promise<Response> {
  return fetch('https://vitest.dev/');
}
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>

导入模块,绕过所有是否需要模拟的检查。如果你想部分模拟模块,这会非常有用。

ts
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 相同,但不会被提升至文件顶部。该模块的下一次导入将导入原始模块而非模拟模块。这不会取消对之前已导入模块的模拟。

ts
export function increment(number) {
  return number + 1;
}
ts
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

通过清除所有模块的缓存来重置模块注册。这允许模块在重新导入时被重新评估。顶层导入无法被重新评估。这对于隔离测试之间存在本地状态冲突的模块可能很有用。

ts
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 ​

等待所有导入加载完毕。如果你有一个同步调用,它开始导入一个无法以其他方式等待的模块,这会非常有用。

ts
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。

ts
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() 模拟模块导出相同的方式,深度模拟指定对象的属性和方法。详情请参阅自动模拟。

ts
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()。此方法返回一个模拟函数。

ts
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。这对于被侦听的方法特别有用:

ts
it('调用 console.log', () => {
  using spy = vi.spyOn(console, 'log').mockImplementation(() => {})
  debug('message')
  expect(spy).toHaveBeenCalled()
})
// console.log 在这里被恢复

TIP

你可以在 afterEach 中调用 vi.restoreAllMocks(或启用 test.restoreMocks)以将所有方法恢复到其原始实现。这将恢复原始的对象描述符,因此将无法更改方法的实现:

ts
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 }) 来侦听每个导出的方法。这将模拟每个导出,但保持其实现不变,允许你断言方法是否被正确调用。

ts
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 恢复其原始值。

ts
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 来恢复之前的值:

ts
import.meta.env.MODE = 'test';

vi.unstubAllEnvs ​

  • 类型: () => Vitest

恢复所有通过 vi.stubEnv 更改的 import.meta.env 和 process.env 变量值。首次调用时,Vitest 会记住原始值并将其保存,直到再次调用 unstubAllEnvs。

ts
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 恢复其原始数值。

ts
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 来恢复原始值:

ts
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。

ts
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

此方法将触发每个已启动的计时器,直到经过指定毫秒数或队列为空(以先发生者为准)。

ts
let i = 0;
setInterval(() => console.log(++i), 50);

vi.advanceTimersByTime(150);

// log: 1
// log: 2
// log: 3

vi.advanceTimersByTimeAsync ​

  • 类型: (ms: number) => Promise<Vitest>

此方法将触发每个已启动的计时器,直到经过指定毫秒数或队列为空(以先发生者为准)。这将包括异步设置的计时器。

ts
let i = 0;
setInterval(() => Promise.resolve().then(() => console.log(++i)), 50);

await vi.advanceTimersByTimeAsync(150);

// log: 1
// log: 2
// log: 3

vi.advanceTimersToNextTimer ​

  • 类型: () => Vitest

将触发下一个可用的计时器。这对于在每次计时器调用之间进行断言很有用。你可以链式调用它来手动管理计时器。

ts
let i = 0;
setInterval(() => console.log(++i), 50);

vi.advanceTimersToNextTimer() // log: 1
  .advanceTimersToNextTimer() // log: 2
  .advanceTimersToNextTimer(); // log: 3

vi.advanceTimersToNextTimerAsync ​

  • 类型: () => Promise<Vitest>

将触发下一个可用的计时器,如果它是异步设置的,则等待其解决。这对于在每次计时器调用之间进行断言很有用。

ts
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 调度的回调所需的毫秒数来推进计时器。

ts
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 配置)。

ts
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 配置)。

ts
setTimeout(async () => {
  console.log(await Promise.resolve('result'));
}, 100);

await vi.runAllTimersAsync();

// log: result

vi.runOnlyPendingTimers ​

  • 类型: () => Vitest

此方法将触发在 vi.useFakeTimers 调用之后启动的每个计时器。它不会触发在其自身调用期间启动的任何计时器。

ts
let i = 0;
setInterval(() => console.log(++i), 50);

vi.runOnlyPendingTimers();

// log: 1

vi.runOnlyPendingTimersAsync ​

  • 类型: () => Promise<Vitest>

此方法将异步触发在 vi.useFakeTimers 调用之后启动的每个计时器,甚至是异步计时器。它不会触发在其自身调用期间启动的任何计时器。

ts
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 相同的字符串和数字参数。

ts
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 }。

这在需要等待异步操作完成时非常有用,例如,当你启动服务器并需要等待其启动时。

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

它也适用于异步回调

ts
// @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 等待元素出现在页面上,然后我们可以对元素做一些事情。

ts
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 语句都会被提升到文件顶部,因此在导入之前定义的任何代码实际上都会在导入评估之后执行。

然而,在导入模块之前执行一些副作用(如模拟日期)可能很有用。

为了绕过这个限制,你可以将静态导入重写为动态导入,如下所示:

diff
callFunctionWithSideEffect()
- import { value } from './some/module.js'
+ const { value } = await import('./some/module.js')

运行 vitest 时,可以使用 vi.hoisted 方法自动完成此操作。在底层,Vitest 会将静态导入转换为动态导入,并保留实时绑定。

diff
- callFunctionWithSideEffect()
import { value } from './some/module.js'
+ vi.hoisted(() => callFunctionWithSideEffect())

无法使用导入

在导入之前运行代码意味着无法访问导入的变量,因为它们尚未定义:

ts
import { value } from './some/module.js';

vi.hoisted(() => { value }); // 抛出错误

此代码将引发错误:

Cannot access '__vi_import_0__' before initialization

如果需要在 vi.hoisted 内部访问另一个模块的变量,请使用动态导入:

ts
await vi.hoisted(async () => {
  const { value } = await import('./some/module.js');
});

然而,不建议在 vi.hoisted 内部导入任何东西,因为导入已经被提升了——如果你需要在测试运行之前执行某些操作,只需在导入的模块本身中执行即可。

此方法返回从工厂函数返回的值。你可以在 vi.mock 工厂函数中使用该值,如果你需要轻松访问本地定义的变量:

ts
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,此方法也可以异步调用:

ts
const json = await vi.hoisted(async () => {
  const response = await fetch('https://jsonplaceholder.typicode.com/posts');
  return response.json();
});

vi.setConfig ​

  • 类型:RuntimeConfig

更新当前测试文件的配置。此方法仅支持影响当前测试文件的配置选项:

ts
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,这将把配置重置为原始状态。

Pager
上一页模拟函数
下一页expect

基于 MIT 许可证 发布。

版权所有 (c) 2021-Present Vitest Team

https://vitest.dev/api/vi

基于 MIT 许可证 发布。

版权所有 (c) 2021-Present Vitest Team