模擬(Mocking)
在編寫測試時,您經常需要建立內部或外部服務的「假」版本。這通常被稱為 模擬(mocking)。Vitest 透過其 vi 輔助函式提供實用工具來協助您。您可以 import { vi } from 'vitest'
或全域存取它(當 全域配置 啟用時)。
WARNING
請務必在每次測試執行前或後清除或還原模擬,以撤銷測試之間的模擬狀態變更!有關更多資訊,請參閱 mockReset
文件。
如果您想直接深入了解,請查看 API 章節;否則,請繼續閱讀以更深入了解模擬的世界。
日期
有時您需要控制日期,以確保測試時的一致性。Vitest 使用 @sinonjs/fake-timers
套件來操作計時器以及系統日期。您可以在 此處 找到有關特定 API 的詳細資訊。
範例
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const businessHours = [9, 17];
function purchase() {
const currentHour = new Date().getHours();
const [open, close] = businessHours;
if (currentHour > open && currentHour < close) return { message: 'Success' };
return { message: 'Error' };
}
describe('purchasing flow', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
expect(purchase()).toEqual({ message: 'Error' });
});
});
函式
模擬函式可以分為兩個不同的類別:偵測(spying) 和 模擬(mocking)。
有時您只需要驗證是否已呼叫特定函式(以及可能傳遞了哪些引數)。在這些情況下,間諜(spy)將是您需要的,您可以使用 vi.spyOn()
(在此處閱讀更多)。
然而,偵測(spy)只能幫助您**偵測(spy)**函式,無法變更這些函式的實作。如果我們需要建立函式的假(或模擬)版本,我們可以使用 vi.fn()
(在此處閱讀更多)。
我們使用 Tinyspy 作為模擬函式的基礎,但我們有自己的包裝器,使其與 jest
相容。vi.fn()
和 vi.spyOn()
共享相同的方法,但是只有 vi.fn()
的返回結果是可呼叫的。
範例
import { afterEach, describe, expect, it, vi } from 'vitest';
function getLatest(index = messages.items.length - 1) {
return messages.items[index];
}
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // 也可以是 `如果支援 getter 或 setter 的話`
};
describe('reading messages', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('should get the latest message with a spy', () => {
const spy = vi.spyOn(messages, 'getLatest');
expect(spy.getMockName()).toEqual('getLatest');
expect(messages.getLatest()).toEqual(
messages.items[messages.items.length - 1]
);
expect(spy).toHaveBeenCalledTimes(1);
spy.mockImplementationOnce(() => 'access-restricted');
expect(messages.getLatest()).toEqual('access-restricted');
expect(spy).toHaveBeenCalledTimes(2);
});
it('should get with a mock', () => {
const mock = vi.fn().mockImplementation(getLatest);
expect(mock()).toEqual(messages.items[messages.items.length - 1]);
expect(mock).toHaveBeenCalledTimes(1);
mock.mockImplementationOnce(() => 'access-restricted');
expect(mock()).toEqual('access-restricted');
expect(mock).toHaveBeenCalledTimes(2);
expect(mock()).toEqual(messages.items[messages.items.length - 1]);
expect(mock).toHaveBeenCalledTimes(3);
});
});
更多
全域變數
您可以使用 vi.stubGlobal
輔助函式來模擬 jsdom
或 node
中不存在的全域變數。它會將全域變數的值放入 globalThis
物件中。
import { vi } from 'vitest';
const IntersectionObserverMock = vi.fn(() => ({
disconnect: vi.fn(),
observe: vi.fn(),
takeRecords: vi.fn(),
unobserve: vi.fn(),
}));
vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
// now you can access it as `IntersectionObserver` or `window.IntersectionObserver`
模組
模擬模組可以監控在其他程式碼中呼叫的第三方函式庫,使您可以測試引數、輸出,甚至重新宣告其實作。
有關更深入的 API 描述,請參閱 vi.mock()
api 章節。
自動模擬演算法
如果您的程式碼匯入了一個模組進行模擬,但沒有任何相關的 __mocks__
檔案或此模組的 factory
,Vitest 將透過調用它並模擬每個匯出的模組。
以下原則適用:
- 所有陣列都將被清空
- 所有原始類型和集合將保持不變
- 所有物件都將被深度複製
- 類別的所有實例及其原型都將被深度複製
範例
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// handlers
export function success(data) {}
export function failure(data) {}
// get todos
export async function getTodos(event, context) {
const client = new Client({
// ...clientOptions
});
await client.connect();
try {
const result = await client.query('SELECT * FROM todos;');
client.end();
return success({
message: `${result.rowCount} item(s) returned`,
data: result.rows,
status: true,
});
} catch (e) {
console.error(e.stack);
client.end();
return failure({ message: e, status: false });
}
}
vi.mock('pg', () => {
const Client = vi.fn();
Client.prototype.connect = vi.fn();
Client.prototype.query = vi.fn();
Client.prototype.end = vi.fn();
return { Client };
});
vi.mock('./handlers.js', () => {
return {
success: vi.fn(),
failure: vi.fn(),
};
});
describe('get a list of todo items', () => {
let client;
beforeEach(() => {
client = new Client();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should return items successfully', async () => {
client.query.mockResolvedValueOnce({ rows: [], rowCount: 0 });
await getTodos();
expect(client.connect).toBeCalledTimes(1);
expect(client.query).toBeCalledWith('SELECT * FROM todos;');
expect(client.end).toBeCalledTimes(1);
expect(success).toBeCalledWith({
message: '0 item(s) returned',
data: [],
status: true,
});
});
it('should throw an error', async () => {
const mError = new Error('Unable to retrieve rows');
client.query.mockRejectedValueOnce(mError);
await getTodos();
expect(client.connect).toBeCalledTimes(1);
expect(client.query).toBeCalledWith('SELECT * FROM todos;');
expect(client.end).toBeCalledTimes(1);
expect(failure).toBeCalledWith({ message: mError, status: false });
});
});
請求
由於 Vitest 在 Node 中執行,因此模擬網路請求相當棘手;Web API 不可用,因此我們需要一些能夠模擬網路行為的工具。我們建議使用 Mock Service Worker 來完成此操作。它將讓您模擬 REST
和 GraphQL
網路請求,並且與框架無關。
Mock Service Worker (MSW) 透過攔截您的測試發出的請求來工作,使您無需更改任何應用程式程式碼即可使用它。在瀏覽器中,這使用 Service Worker API。在 Node.js 中,對於 Vitest,它使用 node-request-interceptor。若要瞭解有關 MSW 的更多資訊,請閱讀他們的 簡介
配置
您可以在您的 設定檔 中像下面這樣使用它
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { graphql, rest } from 'msw';
const posts = [
{
userId: 1,
id: 1,
title: 'first post title',
body: 'first post body',
},
// ...
];
export const restHandlers = [
rest.get('https://rest-endpoint.example/path/to/posts', (req, res, ctx) => {
return res(ctx.status(200), ctx.json(posts));
}),
];
const graphqlHandlers = [
graphql.query(
'https://graphql-endpoint.example/api/v1/posts',
(req, res, ctx) => {
return res(ctx.data(posts));
}
),
];
const server = setupServer(...restHandlers, ...graphqlHandlers);
// Start server before all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Close server after all tests
afterAll(() => server.close());
// Reset handlers after each test `important for test isolation`
afterEach(() => server.resetHandlers());
使用
onUnhandleRequest: 'error'
配置伺服器可確保在沒有相應請求處理常式的請求時拋出錯誤。
範例
我們有一個使用 MSW 的完整工作範例:使用 MSW 進行 React 測試。
更多
MSW 還有更多功能。您可以存取 Cookie 和查詢參數、定義模擬錯誤回應等等!若要查看您可以使用 MSW 執行的所有操作,請閱讀 他們的文件。
計時器
當我們測試涉及逾時或間隔的程式碼時,我們可以透過使用模擬呼叫 setTimeout
和 setInterval
的「假」計時器來加速我們的測試,而不是讓我們的測試等待或逾時。
有關更深入的 API 描述,請參閱 vi.useFakeTimers
api 章節。
範例
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2); // 2 hours
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60); // 1 minute
}
const mock = vi.fn(() => console.log('executed'));
describe('delayed execution', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should execute the function', () => {
executeAfterTwoHours(mock);
vi.runAllTimers();
expect(mock).toHaveBeenCalledTimes(1);
});
it('should not execute the function', () => {
executeAfterTwoHours(mock);
// advancing by 2ms won't trigger the func
vi.advanceTimersByTime(2);
expect(mock).not.toHaveBeenCalled();
});
it('should execute every minute', () => {
executeEveryMinute(mock);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(1);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(2);
});
});
速查表
INFO
以下範例中的 vi
是直接從 vitest
匯入的。 如果您在 config 中將 globals
設置為 true
,則可以全域使用它。
我想要…
- 監視一個
method
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- 模擬導出的變數
// some-path.js
export const getter = 'variable';
// some-path.test.ts
import * as exports from './some-path.js';
vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocked');
- 模擬導出的函數
使用 vi.mock
的範例:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
請注意 vi.mock
的調用會被提升到文件的頂部。 不要 將 vi.mock
調用放在 beforeEach
裡面,只有第一個呼叫會實際生效。
使用 vi.spyOn
的範例:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- 模擬導出的類別實現
使用 vi.mock
和 prototype 的範例:
// some-path.ts
export class SomeClass {}
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
return { SomeClass };
});
// SomeClass.mock.instances 將會包含 SomeClass 的實例
使用 vi.mock
和返回值的範例:
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// SomeClass.mock.returns 將會包含返回的物件
使用 vi.spyOn
的範例:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// 任何適合前兩個範例的實作
});
- 監視從函數返回的物件
使用緩存的範例:
// some-path.ts
export function useObject() {
return { method: () => true };
}
// useObject.js
import { useObject } from './some-path.js';
const obj = useObject();
obj.method();
// useObject.test.js
import { useObject } from './some-path.js';
vi.mock('./some-path.js', () => {
let _cache;
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
};
}
// 現在每次調用 useObject() 都會
// 返回相同的物件引用
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method 在 some-path 內部被調用
expect(obj.method).toHaveBeenCalled();
- 模擬模組的一部分
import { mocked, original } from './some-path.js';
vi.mock('./some-path.js', async () => {
const mod = await vi.importActual<typeof import('./some-path.js')>(
'./some-path.js'
);
return {
...mod,
mocked: vi.fn(),
};
});
original(); // 具有原始行為
mocked(); // 是一個 spy 函式
- 模擬目前日期
要模擬 Date
的時間,您可以使用 vi.setSystemTime
輔助函數。 這個值不會在不同的測試之間自動重置。
請注意,使用 vi.useFakeTimers
也會更改 Date
的時間。
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// 重置模擬時間
vi.useRealTimers();
- 模擬全域變數
您可以透過將值賦值給 globalThis
或使用 vi.stubGlobal
輔助函數來設置全域變數。 當使用 vi.stubGlobal
時,它不會在不同的測試之間自動重置,除非您啟用 unstubGlobals
配置選項或調用 vi.unstubAllGlobals
。
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
- 模擬
import.meta.env
要更改環境變數,您可以直接為其賦予一個新值。 這個值不會在不同的測試之間自動重置。
import { beforeEach, expect, it } from 'vitest';
// 您可以在 beforeEach hook 中手動重置它
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('changes value', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
如果您想要自動重置值,您可以啟用 unstubEnvs
配置選項來使用 vi.stubEnv
輔助函數(或在 beforeEach
hook 中手動調用 vi.unstubAllEnvs
):
import { expect, it, vi } from 'vitest';
// 在執行測試之前 "VITE_ENV" 是 "test"
import.meta.env.VITE_ENV === 'test';
it('changes value', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('the value is restored before running an other test', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};