Vi
Vitest 透過其 vi
輔助工具提供實用工具函式來協助您。您可以全域使用它(當 全域配置 被啟用時),或從 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()
方法。這將清除 mock 歷史記錄,但不會將其實現重設為預設值。
vi.clearAllTimers
移除所有已排程執行的計時器。這些計時器將永遠不會在未來執行。
vi.dynamicImportSettled
如果您有一個同步呼叫啟動了模組的匯入,且您需要等待該匯入完成,這會很有用。
vi.fn
類型:
(fn?: Function) => Mock
建立一個函式的間諜函式 (spy),即使不使用函式也能初始化。每次呼叫函式時,它都會儲存其呼叫參數、回傳值和實例。此外,您可以使用 方法 來操作其行為。 如果沒有提供函式,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
設定的模擬當前日期。如果日期未被 mock,將傳回null
。
vi.getRealSystemTime
類型:
() => number
當使用
vi.useFakeTimers
時,Date.now
的呼叫會被 mock。如果您需要取得以毫秒為單位的實際時間,您可以呼叫此函式。
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())
此方法傳回從 factory 傳回的值。如果需要輕鬆存取本機定義的變數,您可以在
vi.mock
的 factory 中使用該值: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
將從提供的
path
匯入的所有模組替換為另一個模組。您可以在路徑中使用已配置的 Vite 別名。對vi.mock
的呼叫會被提升 (hoisted),因此呼叫它的位置並不重要。它將始終在所有匯入之前執行。如果您需要引用其範圍之外的一些變數,您可以在vi.hoisted
中定義它們,並在vi.mock
中引用它們。WARNING
vi.mock
僅適用於使用import
關鍵字匯入的模組。它不適用於require
。Vitest 靜態分析您的檔案以提升
vi.mock
。這表示您不能使用未直接從vitest
套件匯入的vi
(例如,從其他工具函式檔案匯入)。要解決此問題,請始終使用從vitest
匯入的vi
使用vi.mock
,或啟用globals
配置選項。如果定義了
factory
,則所有匯入都將傳回其結果。Vitest 僅呼叫 factory 一次,並快取結果以供所有後續匯入使用,直到呼叫vi.unmock
或vi.doUnmock
。與
jest
不同,factory 可以是非同步的,因此您可以使用vi.importActual
或在其中接收到的 helper 作為第一個引數,以取得原始模組。tsvi.mock('./path/to/module.js', async importOriginal => { const mod = await importOriginal(); return { ...mod, // 取代一些 exports namedExport: vi.fn(), }; });
WARNING
vi.mock
會被提升(換句話說,移動)到檔案的頂部。這表示無論您在哪裡編寫它(無論是在beforeEach
還是test
中),它實際上都會在那之前被呼叫。這也表示您不能在 factory 中使用在 factory 外部定義的任何變數。
如果您需要在 factory 中使用變數,請嘗試
vi.doMock
。它的工作方式相同,但不會被提升。請注意,它只會 mock 後續的匯入。如果
vi.hoisted
方法在vi.mock
之前宣告,您也可以參考它定義的變數: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
如果您要模擬 (mock) 具有預設匯出的模組,您需要在回傳的 factory 函式物件中提供一個
default
鍵。這是一個 ES 模組特有的特殊情況,因此jest
文件可能會有所不同,因為jest
使用 CommonJS 模組。例如,tsvi.mock('./path/to/module.js', () => { return { default: { myDefaultKey: vi.fn() }, namedExport: vi.fn(), // 等等... }; });
如果有一個
__mocks__
資料夾與您要 mock 的檔案並排,並且未提供 factory,Vitest 將嘗試在__mocks__
子資料夾中找到具有相同名稱的檔案,並將其用作實際模組。如果您要 mock 依賴項,Vitest 將嘗試在專案的 root 中找到一個__mocks__
資料夾(預設為process.cwd()
)。您可以透過 deps.moduleDirectories 配置選項告訴 Vitest 依賴項的位置。例如,您有以下檔案結構:
- __mocks__ - axios.js - src __mocks__ - increment.js - increment.js - tests - increment.test.js
如果您在測試檔案中呼叫
vi.mock
而未提供 factory,它將在__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
,則模組不會自動 mock。要模仿 Jest 的自動 mock 行為,您可以為setupFiles
中的每個所需模組呼叫vi.mock
。如果沒有
__mocks__
資料夾或提供 factory,Vitest 將匯入原始模組並自動 mock 其所有匯出。有關套用的規則,請參閱 演算法。
vi.doMock
類型:
(path: string, factory?: () => unknown) => void
與
vi.mock
相同,但它不會被提升到檔案的頂部,因此您可以參考全域檔案範圍中的變數。模組的下一個 動態匯入 將被 mock。這不會 mock 在呼叫此函式之前匯入的模組。
// ./increment.js
export function increment(number) {
return number + 1;
}
import { beforeEach, test } from 'vitest';
import { increment } from './increment.js';
// 模組未被 mock,因為 vi.doMock 尚未被呼叫
increment(1) === 2;
let mockedIncrement = 100;
beforeEach(() => {
// 您可以存取 factory 內的變數
vi.doMock('./increment.js', () => ({ increment: () => ++mockedIncrement }));
});
test('匯入的下一個模組會是模擬 (mock) 的模組', async () => {
// 原始匯入未被 MOCK,因為 vi.doMock 是在匯入後評估的
expect(increment(1)).toBe(2);
const { increment: mockedIncrement } = await import('./increment.js');
// 新的動態匯入傳回 mock 的模組
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>
匯入模組,繞過所有是否該被模擬的檢查。如果您想部分 mock 模組,這會很有用。
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>>
匯入一個模組,其中所有屬性(包括巢狀屬性)都被 mock。遵循與
vi.mock
相同的規則。有關套用的規則,請參閱 演算法。
vi.resetAllMocks
將在所有 spies 上呼叫 .mockReset()
方法。這將清除 mock 歷史記錄並將其實現重設為空函式(將傳回 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
不會重設 mocks 註冊表。要清除 mocks 註冊表,請使用 vi.unmock
或 vi.doUnmock
。
vi.restoreAllMocks
將在所有 spies 上呼叫 .mockRestore()
方法。這將清除 mock 歷史記錄並將其實現重設為原始狀態。
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
值。 第一次呼叫時,Vitest 會記住原始值並儲存,直到再次呼叫vi.unstubAllEnvs
。
import { vi } from 'vitest';
// `process.env.NODE_ENV` 和 `import.meta.env.NODE_ENV`
// 在呼叫 stubEnv 之前是 "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 起
還原
globalThis
/global
(以及如果您使用jsdom
或happy-dom
環境,則還原window
/top
/self
/parent
) 上所有使用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
此方法會調用每個已啟動的計時器,直到計時器佇列為空。 這表示在
runAllTimers
執行期間呼叫的每個計時器都會被觸發。 如果有無限間隔,則會在嘗試 10,000 次後拋出錯誤。 例如,這將記錄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>
此方法會非同步地調用每個已啟動的計時器,直到計時器佇列為空。 這表示在
runAllTimersAsync
執行期間呼叫的每個計時器都會被觸發,即使是異步計時器。 如果有無限間隔,則會在嘗試 10,000 次後拋出錯誤。 例如,這將記錄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 行的匯入之前呼叫 factory
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()
為止。當使用
--no-threads
在node:child_process
內部執行 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 起
如果已啟用模擬計時器,則返回
true
。
vi.useRealTimers
類型:
() => Vitest
當計時器測試完成時,您可以呼叫此方法,將模擬計時器恢復為其原始實作。之前執行的所有計時器都不會被還原。
vi.waitFor
- 類型:
<T>(callback: WaitForCallback<T>, options?: number | WaitForOptions) => Promise<T>
- 版本: 自 Vitest 0.34.5 起
等待回呼成功執行。 如果回呼拋出錯誤或返回 rejected promise,則會持續等待,直到成功或逾時。
當您需要等待某些異步動作完成時,這非常有用,例如,當您啟動伺服器並需要等待它啟動時。
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();
});