Mockování
Při psaní testů dříve či později narazíte na potřebu vytvořit "falešnou" verzi interní nebo externí služby. Tomu se obvykle říká mockování. Vitest poskytuje pomocné funkce, které vám s tím pomohou, prostřednictvím svého pomocníka vi. Můžete import { vi } from 'vitest'
nebo k němu přistupovat globálně (pokud je globální konfigurace povolena).
WARNING
Nezapomeňte vždy smazat nebo obnovit mocky před nebo po každém spuštění testu, abyste zrušili změny stavu mocků mezi spuštěními! Další informace naleznete v dokumentaci mockReset
.
Pokud se chcete rovnou pustit do práce, podívejte se na sekci API. Jinak pokračujte ve čtení a prozkoumejte svět mockování podrobněji.
Datum a čas
Někdy potřebujete mít kontrolu nad časem, abyste zajistili konzistenci během testování. Vitest používá balíček @sinonjs/fake-timers
pro manipulaci s časovači a systémovým datem. Více informací o konkrétním API naleznete zde.
Příklad
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(() => {
// informujeme Vitest, že používáme mockovaný čas
vi.useFakeTimers();
});
afterEach(() => {
// obnovení data po každém spuštění testu
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
// nastavíme hodinu v rámci pracovní doby
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// přístup k Date.now() vrátí výše nastavené datum
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
// nastavíme hodinu mimo pracovní dobu
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// přístup k Date.now() vrátí výše nastavené datum
expect(purchase()).toEqual({ message: 'Error' });
});
});
Funkce
Mockování funkcí lze rozdělit do dvou kategorií: spying & mocking (sledování a napodobování).
Někdy potřebujete pouze ověřit, zda byla konkrétní funkce volána (a případně s jakými argumenty). V těchto případech by nám stačilo špehování, které můžete použít přímo s vi.spyOn()
(více informací zde).
Špehovací funkce vám umožňují pouze sledovat chování funkcí, nikoli měnit jejich implementaci. V případě, že potřebujeme vytvořit falešnou verzi funkce (nebo mockovanou), můžeme použít vi.fn()
(více informací zde).
Používáme Tinyspy jako základ pro mockování funkcí, ale máme vlastní obal, aby byl kompatibilní s jest
. Jak vi.fn()
, tak vi.spyOn()
sdílejí stejné metody, nicméně pouze návratová hodnota vi.fn()
je volatelná.
Příklad
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);
});
});
Více
Globální objekty
Globální proměnné, které nejsou přítomny v jsdom
nebo node
, můžete mockovat pomocí pomocníka vi.stubGlobal
. Vloží hodnotu globální proměnné do objektu 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);
// nyní k němu můžete přistupovat jako `IntersectionObserver` nebo `window.IntersectionObserver`
Moduly
Mockování modulů umožňuje sledovat chování knihoven třetích stran volaných ve vašem kódu, testovat jejich argumenty a výstupy a dokonce i měnit jejich implementaci.
Podrobnější popis API naleznete v sekci vi.mock()
api.
Automatické mockování
Pokud váš kód importuje mockovaný modul bez jakéhokoli přidruženého souboru __mocks__
nebo factory
pro tento modul, Vitest bude mockovat samotný modul jeho vyvoláním a mockováním každého exportu.
Platí následující zásady:
- Všechna pole budou prázdná
- Všechny primitivy a kolekce zůstanou stejné
- Všechny objekty budou hluboce klonovány
- Všechny instance tříd a jejich prototypy budou hluboce klonovány
Příklad
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// Obslužné funkce
export function success(data) {}
export function failure(data) {}
// Získání úkolů
export async function getTodos(event, context) {
const client = new Client({
// ...možnosti klienta
});
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 });
});
});
Požadavky
Protože Vitest běží v Node.js, mockování síťových požadavků je složité; webová API nejsou k dispozici, takže potřebujeme něco, co bude napodobovat chování sítě. Doporučujeme Mock Service Worker k dosažení tohoto cíle. Umožní vám mockovat síťové požadavky REST
i GraphQL
a je nezávislý na frameworku.
Mock Service Worker (MSW) funguje tak, že zachycuje požadavky, které vaše testy provádějí, což vám umožňuje jej používat bez změny kódu vaší aplikace. V prohlížeči to používá Service Worker API. V Node.js a pro Vitest používá node-request-interceptor. Chcete-li se dozvědět více o MSW, přečtěte si jejich úvod.
Konfigurace
Můžete jej použít následujícím způsobem ve vašem konfiguračním souboru.
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);
// Spuštění serveru před všemi testy
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Ukončení serveru po všech testech
afterAll(() => server.close());
// Reset handlerů po každém testu (důležité pro izolaci testů)
afterEach(() => server.resetHandlers());
Konfigurace serveru s
onUnhandleRequest: 'error'
zajišťuje, že bude vyvolána chyba, kdykoli dojde k požadavku, který nemá odpovídající obslužnou rutinu.
Příklad
Máme plně funkční příklad, který používá MSW: React Testing with MSW.
Více
S MSW toho můžete dělat mnohem více. Můžete přistupovat k souborům cookie a parametrům dotazu, definovat mockované chybové odpovědi a mnoho dalšího! Pro více informací o MSW si přečtěte dokumentaci.
Časovače
Když testujeme kód, který zahrnuje časové intervaly nebo intervaly, místo toho, abychom nechali naše testy čekat nebo vypršet časový limit, můžeme naše testy urychlit pomocí "falešných" časovačů, které mockují volání setTimeout
a setInterval
.
Podrobnější popis API naleznete v sekci vi.useFakeTimers
api.
Příklad
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);
// posun o 2ms funkci nespustí
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);
});
});
Cheat Sheet
INFO
vi
v níže uvedených příkladech je importováno přímo z vitest
. Můžete ho také používat globálně, pokud nastavíte globals
na true
ve vaší konfiguraci.
Chci…
- Špehovat metodu instance
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- Mockovat exportované proměnné
// 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');
- Mockovat exportovanou funkci
Příklad použití vi.mock
:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
Nezapomeňte, že volání vi.mock
je přesunuto na začátek souboru (tzv. hoisting). Nevkládejte volání vi.mock
do bloku beforeEach
, protože pouze jedno z nich by skutečně mockovalo modul.
Příklad s vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- Mockovat implementaci exportované třídy
Příklad s vi.mock
a prototypem:
// 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 bude obsahovat instance třídy SomeClass
Příklad s vi.mock
a návratovou hodnotou:
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// SomeClass.mock.returns bude obsahovat vrácené objekty
Příklad s vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// cokoliv vám vyhovuje z prvních dvou příkladů
});
- Špehovat objekt vrácený z funkce
Příklad použití cache:
// 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(),
};
}
// nyní pokaždé, když je volána funkce useObject(), vrátí stejnou referenci objektu
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method bylo voláno uvnitř some-path
expect(obj.method).toHaveBeenCalled();
- Mockovat část modulu
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(); // má původní chování
mocked(); // je špehovací funkce
- Mockovat aktuální datum
Pro mockování času Date
můžete použít pomocnou funkci vi.setSystemTime
. Tato hodnota se neresetuje automaticky mezi jednotlivými testy.
Mějte na paměti, že použití vi.useFakeTimers
také změní čas Date
.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// reset mockovaného času
vi.useRealTimers();
- Mockovat globální proměnnou
Globální proměnnou můžete nastavit přiřazením hodnoty do globalThis
nebo pomocí pomocníka vi.stubGlobal
. Pokud použijete vi.stubGlobal
, tato hodnota se neresetuje automaticky mezi jednotlivými testy, pokud nepovolíte volbu konfigurace unstubGlobals
nebo nezavoláte vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
- Mockovat
import.meta.env
Pro změnu proměnné prostředí můžete jednoduše přiřadit novou hodnotu proměnné import.meta.env
. Tato hodnota se neresetuje automaticky mezi jednotlivými testy.
import { beforeEach, expect, it } from 'vitest';
// můžete ji resetovat v beforeEach hooku manuálně
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');
});
Pokud chcete automaticky resetovat hodnotu, můžete použít pomocníka vi.stubEnv
s povolenou volbou konfigurace unstubEnvs
(nebo zavolat vi.unstubAllEnvs
manuálně v beforeEach
hooku):
import { expect, it, vi } from 'vitest';
// před spuštěním testů je "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 another test', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};