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 不會模擬在 設定檔案中匯入的模組,因為在測試檔案執行時它們已被快取。您可以在 vi.hoisted
內部呼叫 vi.resetModules()
以在執行測試檔案之前清除所有模組快取。
如果定義了 factory
函數,所有匯入都將返回其結果。Vitest 只呼叫 factory 一次,並為所有後續匯入快取結果,直到呼叫 vi.unmock
或 vi.doUnmock
。
與 jest
不同,factory 可以是異步的。您可以使用 vi.importActual
或將 factory 作為第一個參數傳入的輔助函數,並在內部取得原始模組。
您也可以提供一個帶有 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 仍然在字串而不是模組物件上操作。
但是,如果您在 tsconfig.json
中配置了 paths
別名並使用 TypeScript,編譯器將無法正確解析匯入類型。 為了使其正常運作,請確保將所有別名匯入替換為其對應的相對路徑。 例如,使用 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 將嘗試在專案的 根目錄(預設為 process.cwd()
)中找到一個 __mocks__
資料夾。您可以透過 deps.moduleDirectories
配置選項告訴 Vitest 依賴項的位置。
例如,您有以下檔案結構:
- __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__
資料夾或未提供 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';
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 整個物件都被模擬了,如果它確實如此。
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
從模擬註冊表中移除模組。所有匯入呼叫都將返回原始模組,即使它之前已被模擬。此呼叫會被提升到檔案頂部,因此它只會取消模擬在設定檔案中定義的模組,例如。
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;
// 這被提升了,並且 factory 在第 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(() => {
vi.resetModules();
});
test('改變狀態', async () => {
const mod = await import('./some/path.js'); // 將會重新評估
mod.changeLocalState('新值');
expect(mod.getLocalState()).toBe('新值');
});
test('模組具有舊狀態', async () => {
const mod = await import('./some/path.js'); // 將會重新評估
expect(mod.getLocalState()).toBe('舊值');
});
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
在函數上建立一個監聽器,儘管可以不帶函數啟動。每次呼叫函數時,它都會儲存其呼叫參數、返回值和實例。此外,您可以使用方法來操作其行為。 如果沒有給定函數,則在呼叫時,模擬將返回 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
在所有監聽器上呼叫 .mockClear()
。 這將清除模擬歷史,而不影響模擬實作。
vi.resetAllMocks
在所有監聽器上呼叫 .mockReset()
。 這將清除模擬歷史並將每個模擬的實作重設為其原始狀態。
vi.restoreAllMocks
在所有監聽器上呼叫 .mockRestore()
。 這將清除模擬歷史,還原所有原始模擬實作,並還原被監聽物件的原始描述符。
vi.spyOn
- 類型:
<T, K extends keyof T>(object: T, method: K, accessType?: 'get' | 'set') => MockInstance
在物件的方法或 getter/setter 上建立一個監聽器,類似於 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
在支援 Explicit Resource Management 的環境中,您可以使用 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,它將繼續等待直到成功或超時。
如果選項設定為數字,則效果等同於設定 { 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
內部匯入任何東西,因為匯入已經被提升了——如果您需要在測試執行之前執行某些操作,只需在匯入的模組本身中執行即可。
此方法返回從 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 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
,這將把配置重設為原始狀態。