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í principy:
- Všechna pole budou vyprázdněna
- Všechny primitivy a kolekce zůstanou stejné
- Všechny objekty budou hluboce naklonovány
- Všechny instance tříd a jejich prototypy budou hluboce naklonovány
Virtuální moduly
Vitest podporuje mockování Vite virtuálních modulů. Funguje to jinak než u virtuálních modulů v Jestu. Místo předávání virtual: true
do funkce vi.mock
musíte říct Vite, že modul existuje, jinak se to při parsování nezdaří. Můžete to udělat několika způsoby:
- Poskytnutí aliasu
// vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js'),
},
},
};
- Poskytnutí pluginu, který vyřeší (resolve) virtuální modul
// vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms') return 'virtual:$app/forms';
},
},
],
};
Výhodou druhého přístupu je, že můžete dynamicky vytvářet různé virtuální vstupní body. Pokud přesměrujete několik virtuálních modulů do jednoho souboru, budou všechny ovlivněny funkcí vi.mock
, takže se ujistěte, že používáte jedinečné identifikátory.
Úskalí mockování
Mějte na paměti, že není možné mockovat volání metod, které jsou volány uvnitř jiných metod ve stejném souboru. Například v tomto kódu:
export function foo() {
return 'foo';
}
export function foobar() {
return `${foo()}bar`;
}
Není možné mockovat metodu foo
zvenčí, protože je na ni přímo odkazováno. Takže tento kód nebude mít žádný vliv na volání foo
uvnitř foobar
(ale ovlivní volání foo
v jiných modulech):
import { vi } from 'vitest';
import * as mod from './foobar.js';
// toto ovlivní pouze "foo" mimo původní modul
vi.spyOn(mod, 'foo');
vi.mock('./foobar.js', async importOriginal => {
return {
...(await importOriginal<typeof import('./foobar.js')>()),
// toto ovlivní pouze "foo" mimo původní modul
foo: () => 'mocked',
};
});
Toto chování si můžete ověřit přímým poskytnutím implementace metodě foobar
:
// foobar.test.js
import * as mod from './foobar.js';
vi.spyOn(mod, 'foo');
// exportované foo odkazuje na mockovanou metodu
mod.foobar(mod.foo);
// foobar.js
export function foo() {
return 'foo';
}
export function foobar(injectedFoo) {
return injectedFoo !== foo; // false
}
Toto je zamýšlené chování. Obvykle je to známka špatného kódu, pokud je mockování zapojeno takovým způsobem. Zvažte refaktorování kódu do více souborů nebo vylepšení architektury vaší aplikace pomocí technik, jako je vkládání závislostí.
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á @mswjs/interceptors
. 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 { HttpResponse, graphql, http } from 'msw';
const posts = [
{
userId: 1,
id: 1,
title: 'first post title',
body: 'first post body',
},
// ...
];
export const restHandlers = [
http.get('https://rest-endpoint.example/path/to/posts', () => {
return HttpResponse.json(posts);
}),
];
const graphqlHandlers = [
graphql.query('ListPosts', () => {
return HttpResponse.json({
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
:
WARNING
Nezapomeňte, že volání vi.mock
je přesunuto na začátek souboru. Vždy bude provedeno před všemi importy.
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
- 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.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 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 importOriginal => {
const mod = await importOriginal<typeof import('./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
- Chcete-li změnit proměnnou prostředí, můžete jí jednoduše přiřadit novou hodnotu.
WARNING
Hodnota proměnné prostředí se nebude automaticky resetovat mezi různý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');
});
- Chcete-li automaticky resetovat hodnoty, můžete použít pomocníka
vi.stubEnv
se zapnutou možností konfiguraceunstubEnvs
(nebo ručně zavolatvi.unstubAllEnvs
](/api/vi#vi-unstuballenvs) v hookubeforeEach
):
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,
},
};