Mockování
Při psaní testů je jen otázkou času, kdy budete potřebovat vytvořit „fiktivní“ verzi interní – nebo externí – služby. Toto se běžně označuje jako mockování. Vitest poskytuje pomocné funkce, které vá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
Vždy pamatujte na vymazání nebo obnovení mocků před nebo po každém spuštění testu, abyste vrátili stav mocků do původního stavu mezi jednotlivými spuštěními! Další informace naleznete v dokumentaci mockReset
.
Pokud se chcete ihned ponořit do tématu, podívejte se do sekce API, jinak pokračujte ve čtení, abyste se hlouběji ponořili do světa mockování.
Datum a čas
Někdy je potřeba mít kontrolu nad datem, pro zajištění konzistence při testování. Vitest využívá balíček @sinonjs/fake-timers
pro manipulaci s časovači a také se systémovým datem. Více o konkrétním API naleznete podrobně 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(() => {
// informujte 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', () => {
// nastavte hodinu v rámci pracovní doby
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// volání Date.now() vrátí výše nastavené datum
expect(purchase()).toEqual({ message: 'Success' });
});
it('disallows purchases outside of business hours', () => {
// nastavte hodinu mimo pracovní dobu
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// volání Date.now() vrátí výše nastavené datum
expect(purchase()).toEqual({ message: 'Error' });
});
});
Funkce
Mockování funkcí můžeme rozdělit do dvou kategorií: sledování a mockování.
Někdy vše, co potřebujete, je ověřit, zda byla volána konkrétní funkce (a případně jaké argumenty byly předány). V těchto případech postačí špeh, který můžete použít přímo s vi.spyOn()
(více si přečtěte zde).
Sledovače vám však mohou pomoci pouze s špehováním funkcí, nejsou schopny změnit implementaci těchto funkcí. Pokud potřebujeme vytvořit fiktivní (nebo mockovanou) verzi funkce, můžeme použít vi.fn()
(více si přečtěte zde).
Pro mockování funkcí používáme Tinyspy, ale máme vlastní wrapper, aby byl kompatibilní s jest
. Jak vi.fn()
, tak vi.spyOn()
mají stejné metody, nicméně pouze návratový výsledek vi.fn()
lze volat.
Příklad
import { afterEach, describe, expect, it, vi } from 'vitest';
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // může být také `getter` nebo `setter`, je-li podporován
};
function getLatest(index = messages.items.length - 1) {
return messages.items[index];
}
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í proměnné
Globální proměnné, které nejsou přítomny s jsdom
nebo node
, můžete mockovat pomocí pomocníka vi.stubGlobal
. Tento pomocník 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
Mock moduly umožňují sledovat knihovny třetích stran, které jsou vyvolány v jiném kódu, což vám umožňuje testovat argumenty, výstup nebo dokonce předefinovat jejich implementaci.
Podrobnější popis API naleznete v sekci vi.mock()
API.
Algoritmus automatického mockování
Pokud váš kód importuje mockovaný modul bez jakéhokoli přidruženého souboru __mocks__
nebo factory
pro tento modul, Vitest modul sám mockuje tak, že jej vyvolá a mockuje všechny jeho exporty.
Platí následující principy:
- Všechna pole budou vyprázdněna
- Všechny primitivní typy a kolekce zůstanou stejné
- Všechny objekty budou hluboce klonovány
- Všechny instance tříd a jejich prototypy budou hluboce klonovány
Virtuální moduly
Vitest podporuje mockování Vite virtuálních modulů. Funguje to jinak než způsob, jakým jsou virtuální moduly zpracovávány v Jestu. Namísto předávání virtual: true
funkci vi.mock
musíte Vite říct, že modul existuje, jinak selže během parsování. Můžete to udělat několika způsoby:
- Poskytněte alias
// vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js'),
},
},
};
- Poskytněte plugin, který řeší virtuální modul
// vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms') {
return 'virtual:$app/forms';
}
},
},
],
};
Předností 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, pak budou všechny ovlivněny 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 volaných uvnitř jiných metod stejného 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 přímo odkazována. Proto 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í můžete potvrdit 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 chování je záměrné. Obvykle je to známka špatného kódu, když je mockování zapojeno takovým způsobem. Zvažte refaktorování kódu do více souborů nebo zlepšení architektury vaší aplikace pomocí technik, jako je dependency injection.
Příklad
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// Funkce pro získání úkolů
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} položek vráceno`,
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 položek vráceno',
data: [],
status: true,
});
});
it('should throw an error', async () => {
const mError = new Error('Nelze načíst řádky');
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 });
});
});
Souborový systém
Mockování souborového systému zajišťuje, že testy nezávisí na skutečném souborovém systému, a testy se tak stávají spolehlivějšími a předvídatelnějšími. Tato izolace pomáhá předcházet nežádoucím efektům z předchozích testů. Umožňuje testování chybových stavů a hraničních případů, které by bylo obtížné nebo nemožné replikovat se skutečným souborovým systémem, jako jsou problémy s oprávněními, scénáře plného disku nebo chyby čtení/zápisu.
Vitest neposkytuje žádné API pro mockování souborového systému. Můžete použít vi.mock
k ručnímu mockování modulu fs
, ale jeho údržba je obtížná. Místo toho doporučujeme použít memfs
. memfs
vytváří souborový systém v paměti, který simuluje operace souborového systému bez dotyku skutečného disku. Tento přístup je rychlý a bezpečný, vyhýbá se jakýmkoli potenciálním vedlejším účinkům na skutečný souborový systém.
Příklad
Pro automatické přesměrování každého volání fs
na memfs
můžete vytvořit soubory __mocks__/fs.cjs
a __mocks__/fs/promises.cjs
v kořenovém adresáři vašeho projektu:
// můžeme také použít `import`, ale pak
// každý export by měl být explicitně definován
const { fs } = require('memfs');
module.exports = fs;
// můžeme také použít `import`, ale pak
// každý export by měl být explicitně definován
const { fs } = require('memfs');
module.exports = fs.promises;
// read-hello-world.js
import { readFileSync } from 'node:fs';
export function readHelloWorld(path) {
return readFileSync(path);
}
// hello-world.test.js
import { beforeEach, expect, it, vi } from 'vitest';
import { fs, vol } from 'memfs';
import { readHelloWorld } from './read-hello-world.js'; // Opraveno: přidáno `from`
// řekněte Vitestu, aby použil mock fs ze složky __mocks__
// toto lze provést v souboru nastavení, pokud by fs měl být vždy mockován
vi.mock('node:fs');
vi.mock('node:fs/promises');
beforeEach(() => {
// resetujte stav in-memory fs
vol.reset();
});
it('should return correct text', () => {
const path = '/hello-world.txt';
fs.writeFileSync(path, 'hello world');
const text = readHelloWorld(path);
expect(text).toBe('hello world');
});
it('can return a value multiple times', () => {
// můžete použít vol.fromJSON k definování několika souborů
vol.fromJSON(
{
'./dir1/hw.txt': 'hello dir1',
'./dir2/hw.txt': 'hello dir2',
},
// výchozí cwd
'/tmp'
);
expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1');
expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2');
});
Síťové požadavky
Protože Vitest běží v Node, mockování síťových požadavků je složité; webová API zde nejsou dostupná, takže potřebujeme něco, co by simulovalo síťové chování. K tomu doporučujeme Mock Service Worker. Umožní vám mockovat jak REST
, tak GraphQL
síťové požadavky a je nezávislý na frameworku.
Mock Service Worker (MSW) funguje tak, že zachycuje požadavky, které vaše testy provádějí, a umožňuje vám jej používat bez změny jakéhokoli kódu vaší aplikace. V prohlížeči k tomu používá Service Worker API. V Node.js a pro Vitest používá knihovnu @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 jako níže ve vašem souboru nastavení
import { afterAll, afterEach, beforeAll } from 'vitest';
import { setupServer } from 'msw/node';
import { graphql, http, HttpResponse } 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);
// Spusťte server před všemi testy
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Ukončete server po dokončení všech testů
afterAll(() => server.close());
// Resetujte handlery po každém testu (důležité pro izolaci testů)
afterEach(() => server.resetHandlers());
Konfigurace serveru s
onUnhandledRequest: 'error'
zajišťuje, že se vyvolá chyba, kdykoli dojde k požadavku, který nemá odpovídající handler.
Více
MSW toho umí mnohem víc. Můžete přistupovat k cookies a parametrům dotazu, definovat mockované chybové odpovědi a mnoho dalšího! Chcete-li vidět vše, co můžete s MSW dělat, přečtěte si jejich dokumentaci.
Časovače
Když testujeme kód, který zahrnuje timeouty nebo intervaly, namísto toho, abychom nechali naše testy čekat nebo vypršet (timeoutovat), můžeme naše testy urychlit použitím „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 hodiny
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60); // 1 minuta
}
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 nespustí funkci
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);
});
});
Třídy
Celou třídu můžete mockovat jediným voláním vi.fn
– jelikož všechny třídy jsou také funkce, funguje to automaticky. Mějte na paměti, že Vitest v současné době nerespektuje klíčové slovo new
, takže new.target
je v těle funkce vždy undefined
.
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
static getType(): string {
return 'animal';
}
speak(): string {
return 'bark!';
}
isHungry() {}
feed() {}
}
Tuto třídu můžeme znovu vytvořit pomocí funkcí ES5:
const Dog = vi.fn(function (name) {
this.name = name;
});
// všimněte si, že statické metody jsou mockovány přímo na funkci,
// nikoli na instanci třídy
Dog.getType = vi.fn(() => 'mocked animal');
// mockujte metody "speak" a "feed" na každé instanci třídy
// všechny instance `new Dog()` zdědí tyto špehy
Dog.prototype.speak = vi.fn(() => 'loud bark!');
Dog.prototype.feed = vi.fn();
KDY POUŽÍT?
Obecně řečeno, třídu byste takto znovu vytvořili uvnitř továrny modulů, pokud je třída znovu exportována z jiného modulu:
import { Dog } from './dog.js';
vi.mock(import('./dog.js'), () => {
const Dog = vi.fn();
Dog.prototype.feed = vi.fn();
// ... další mocky
return { Dog };
});
Tato metoda může být také použita k předání instance třídy funkci, která přijímá stejné rozhraní:
// ./src/feed.ts
function feed(dog: Dog) {
// ...
}
// ./tests/dog.test.ts
import { expect, test, vi } from 'vitest';
import { feed } from '../src/feed.js';
const Dog = vi.fn();
Dog.prototype.feed = vi.fn();
test('can feed dogs', () => {
const dogMax = new Dog('Max');
feed(dogMax);
expect(dogMax.feed).toHaveBeenCalled();
expect(dogMax.isHungry()).toBe(false);
});
Nyní, když vytvoříme novou instanci třídy Dog
, její metoda speak
(spolu s feed
) je již mockována:
const dog = new Dog('Cooper');
dog.speak(); // hlasitě štěká
// můžete použít vestavěné aserce k ověření platnosti volání
expect(dog.speak).toHaveBeenCalled();
Můžeme znovu přiřadit návratovou hodnotu pro konkrétní instanci:
const dog = new Dog('Cooper');
// "vi.mocked" je pomocník typu, protože
// TypeScript neví, že Dog je mockovaná třída,
// obalí jakoukoli funkci do typu MockInstance<T>
// bez ověření, zda je funkce mock
vi.mocked(dog.speak).mockReturnValue('woof woof');
dog.speak(); // woof woof
Pro mockování vlastnosti můžeme použít metodu vi.spyOn(dog, 'name', 'get')
. To umožňuje používat špionážní aserce na mockované vlastnosti:
const dog = new Dog('Cooper');
const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max');
expect(dog.name).toBe('Max');
expect(nameSpy).toHaveBeenCalledTimes(1);
TIP
Stejnou metodou můžete špehovat i gettery a settery.
Rychlý přehled
INFO
vi
v příkladech níže je importováno přímo z vitest
. Můžete jej také použít globálně, pokud v konfiguraci nastavíte globals
na true
.
Chci…
Mockování exportovaných proměnných
// 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');
Mockování exportované funkce
- Příklad s
vi.mock
:
WARNING
Nezapomeňte, že volání vi.mock
je automaticky přesunuto na začátek souboru. Vždy se provede 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(() => {});
Mockování implementace exportované třídy
- Příklad s
vi.mock
a.prototype
:
// ./some-path.ts
export class SomeClass {}
import { SomeClass } from './some-path.js';
vi.mock(import('./some-path.js'), () => {
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
return { SomeClass };
});
// SomeClass.mock.instances bude mít SomeClass
- Příklad s
vi.spyOn
:
import * as mod from './some-path.js';
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass);
Špehování objektu vráceného z funkce
- Příklad s použitím 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(import('./some-path.js'), () => {
let _cache;
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
};
}
// nyní pokaždé, když je volána useObject(), vrátí
// stejnou referenci na objekt
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method bylo voláno uvnitř some-path
expect(obj.method).toHaveBeenCalled();
Mockování části modulu
import { mocked, original } from './some-path.js';
vi.mock(import('./some-path.js'), async importOriginal => {
const mod = await importOriginal();
return {
...mod,
mocked: vi.fn(),
};
});
original(); // má původní chování
mocked(); // je sledovací funkce
WARNING
Nezapomeňte, že toto mockuje pouze externí přístup. V tomto příkladu, pokud original
volá mocked
interně, vždy zavolá funkci definovanou v modulu, nikoli v továrně mocků.
Mockování aktuálního data
Pro mockování času Date
můžete použít pomocnou funkci vi.setSystemTime
. Tato hodnota se nebude automaticky resetovat mezi různými testy.
Mějte na paměti, že použití vi.useFakeTimers
také mění čas Date
.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// resetovat mockovaný čas
vi.useRealTimers();
Mockování globální proměnné
Globální proměnnou můžete nastavit přiřazením hodnoty globalThis
nebo pomocí pomocníka vi.stubGlobal
. Při použití vi.stubGlobal
se hodnota nebude automaticky resetovat mezi různými testy, pokud nepovolíte konfigurační možnost unstubGlobals
nebo nezavoláte vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
Mockování 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 ručně resetovat v beforeEach hooku
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 hodnotu(y) automaticky resetovat, můžete použít pomocníka
vi.stubEnv
s povolenou konfigurační možnostíunstubEnvs
(nebo ručně zavolatvi.unstubAllEnvs
vbeforeEach
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 an other test', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default defineConfig({
test: {
unstubEnvs: true,
},
});