Мокирование
При написании тестов часто возникает необходимость в создании имитации (мока) внутреннего или внешнего сервиса. Это называется мокированием. 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(() => {
// tell vitest we use mocked time
vi.useFakeTimers();
});
afterEach(() => {
// restoring date after each test run
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
// set hour within business hours
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// access Date.now() will result in the date set above
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
// set hour outside business hours
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// access Date.now() will result in the date set above
expect(purchase()).toEqual({ message: 'Error' });
});
});
Функции
Мокирование функций можно разделить на две категории: шпионаж и мокирование.
Иногда достаточно проверить, была ли вызвана функция, и, возможно, с какими аргументами. В этих случаях шпионажа будет достаточно, и вы можете использовать vi.spyOn()
(подробнее здесь).
Однако шпионы позволяют только отслеживать вызовы функций, но не изменять их реализацию. В случае, когда нам нужно создать фейковую (или мокированную) версию функции, мы можем использовать 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, // can also be a `getter or setter if supported`
};
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);
});
});
Больше информации
Глобальные переменные
Вы можете мокировать глобальные переменные, отсутствующие в jsdom
или node
, используя хелпер vi.stubGlobal
. Он добавит значение глобальной переменной в объект 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 section для более подробного описания 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.js, мокирование сетевых запросов может быть затруднительным из-за отсутствия веб-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: React Testing with MSW.
Больше информации
В MSW есть гораздо больше возможностей. Вы можете получить доступ к файлам cookie и параметрам запроса, определять фиктивные ответы об ошибках и многое другое! Чтобы увидеть все, что вы можете делать с MSW, прочитайте их документацию.
Таймеры
При тестировании кода, использующего тайм-ауты или интервалы, вместо ожидания или истечения времени ожидания, можно ускорить тесты, используя имитацию таймеров, перехватывающую вызовы setTimeout
и setInterval
.
Смотрите раздел API vi.useFakeTimers
api section для более подробного описания 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
. Вы также можете использовать его глобально, если установите для 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
перемещается в начало файла (hoisted). Не размещайте вызовы 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.results будет содержать возвращенные объекты
Пример с vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// whatever suites you from first two examples
});
- Шпионить за объектом, возвращаемым из функции
Пример с использованием кэша:
// 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(); // является шпионской функцией
- Мокировать текущую дату
Чтобы замокать время 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
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');
});
Если вы хотите автоматически сбросить значение, вы можете использовать функцию vi.stubEnv
с включенным параметром конфигурации unstubEnvs
(или вызвать vi.unstubAllEnvs
вручную в хуке beforeEach
):
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('значение восстанавливается перед запуском другого теста', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};