Mocking
Quando si scrivono test, è solo una questione di tempo prima che sia necessario creare una versione "falsa" di un servizio interno o esterno. Questo processo è comunemente chiamato mocking. Vitest fornisce funzioni di utilità per facilitare il mocking tramite il suo helper vi. È possibile importare vi
con import { vi } from 'vitest'
oppure accedervi globalmente (quando la configurazione globale è abilitata).
WARNING
Ricorda sempre di pulire o ripristinare i mock prima o dopo ogni esecuzione del test per annullare le modifiche dello stato del mock tra le diverse esecuzioni! Per ulteriori informazioni, consulta la documentazione di mockReset
.
Se desideri approfondire immediatamente, consulta la sezione API; altrimenti, continua a leggere per esplorare il mondo del mocking.
Date
A volte è necessario controllare la data per garantire la coerenza durante i test. Vitest utilizza il pacchetto @sinonjs/fake-timers
per manipolare i timer e la data di sistema. Puoi trovare maggiori dettagli sull'API specifica qui.
Esempio
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(() => {
// indica a Vitest che stiamo usando i timer mockati
vi.useFakeTimers();
});
afterEach(() => {
// ripristina la data dopo ogni esecuzione del test
vi.useRealTimers();
});
it('consente acquisti durante l\'orario di lavoro', () => {
// imposta l'ora all'interno dell'orario di lavoro
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// l'accesso a Date.now() restituirà la data impostata sopra
expect(purchase()).toEqual({ message: 'Success' });
});
it('non consente acquisti al di fuori dell\'orario di lavoro', () => {
// imposta l'ora al di fuori dell'orario di lavoro
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// l'accesso a Date.now() restituirà la data impostata sopra
expect(purchase()).toEqual({ message: 'Error' });
});
});
Funzioni
Il mocking delle funzioni può essere suddiviso in due categorie distinte: spying e mocking.
A volte tutto ciò di cui hai bisogno è verificare se una funzione specifica è stata chiamata (e possibilmente quali argomenti sono stati passati). In questi casi, una "spy" è sufficiente e puoi usarla direttamente con vi.spyOn()
(leggi di più qui).
Tuttavia, le "spy" possono solo aiutarti a spiare le funzioni; non possono alterare la loro implementazione. Nel caso in cui sia necessario creare una versione fittizia (o mockata) di una funzione, possiamo usare vi.fn()
(leggi di più qui).
Utilizziamo Tinyspy come base per il mocking delle funzioni, ma abbiamo creato un wrapper per garantirne la compatibilità con jest
. Sia vi.fn()
che vi.spyOn()
condividono gli stessi metodi; tuttavia, solo il risultato restituito da vi.fn()
è invocabile.
Esempio
import { afterEach, describe, expect, it, vi } from 'vitest';
const messages = {
items: [
{ message: 'Simple test message', from: 'Testman' },
// ...
],
getLatest, // può anche essere un `getter` o `setter` (se supportato)
};
function getLatest(index = messages.items.length - 1) {
return messages.items[index];
}
describe('reading messages', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('dovrebbe recuperare l\'ultimo messaggio con una 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('dovrebbe recuperare con un 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);
});
});
Altro
Variabili globali
È possibile simulare variabili globali non disponibili in jsdom
o node
utilizzando l'helper vi.stubGlobal
. Questo imposterà il valore della variabile globale nell'oggetto 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);
// ora puoi accedervi come `IntersectionObserver` o `window.IntersectionObserver`
Moduli
I moduli mock permettono di simulare librerie di terze parti invocate in altro codice, consentendoti di testare argomenti, output o persino di ridefinire la loro implementazione.
Consulta la sezione API di vi.mock()
per una descrizione API più dettagliata.
Algoritmo di Automocking
Se il tuo codice sta importando un modulo mockato, senza alcun file __mocks__
associato o factory
per questo modulo, Vitest simulerà il modulo stesso invocandolo e creando un mock per ogni export.
Si applicano le seguenti regole:
- Tutti gli array saranno svuotati
- Tutti i primitivi e le collezioni rimarranno invariati
- Tutti gli oggetti saranno clonati in profondità
- Tutte le istanze di classi e i loro prototipi saranno clonati ricorsivamente
Moduli Virtuali
Vitest supporta il mocking dei moduli virtuali di Vite. Funziona in modo diverso da come i moduli virtuali sono trattati in Jest. Invece di passare virtual: true
a una funzione vi.mock
, devi configurare Vite per riconoscere l'esistenza del modulo, altrimenti fallirà durante il parsing. Puoi farlo in diversi modi:
- Fornire un alias
// vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js'),
},
},
};
- Fornire un plugin che risolve un modulo virtuale
// vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms') {
return 'virtual:$app/forms';
}
},
},
],
};
Il vantaggio del secondo approccio è che puoi creare dinamicamente diversi entrypoint virtuali. Se reindirizzi diversi moduli virtuali in un singolo file, tutti questi saranno influenzati da vi.mock
, quindi assicurati di usare identificatori unici.
Insidie del Mocking
Si noti che non è possibile simulare le chiamate a metodi che vengono chiamati all'interno di altri metodi dello stesso file. Ad esempio, in questo codice:
export function foo() {
return 'foo';
}
export function foobar() {
return `${foo()}bar`;
}
Non è possibile simulare il metodo foo
dall'esterno perché è referenziato direttamente. Quindi questo codice non avrà alcun effetto sulla chiamata a foo
all'interno di foobar
(ma influenzerà la chiamata a foo
in altri moduli):
import { vi } from 'vitest';
import * as mod from './foobar.js';
// questo influenzerà solo "foo" al di fuori del modulo originale
vi.spyOn(mod, 'foo');
vi.mock('./foobar.js', async importOriginal => {
return {
...(await importOriginal<typeof import('./foobar.js')>()),
// questo influenzerà solo "foo" al di fuori del modulo originale
foo: () => 'mocked',
};
});
Puoi confermare questo comportamento fornendo l'implementazione al metodo foobar
direttamente:
// foobar.test.js
import * as mod from './foobar.js';
vi.spyOn(mod, 'foo');
// il metodo `foo` esportato (quando iniettato) fa riferimento al mock
mod.foobar(mod.foo);
// foobar.js
export function foo() {
return 'foo';
}
export function foobar(injectedFoo) {
return injectedFoo === foo; // false
}
Questo è il comportamento previsto. Di solito è un segno di codice di scarsa qualità quando il mocking è coinvolto in questo modo. Considera di rifattorizzare il tuo codice in più file o di migliorare l'architettura della tua applicazione utilizzando tecniche come l'iniezione di dipendenza.
Esempio
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { Client } from 'pg';
import { failure, success } from './handlers.js';
// 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} elemento/i restituito/i`,
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('ottieni una lista di elementi todo', () => {
let client;
beforeEach(() => {
client = new Client();
});
afterEach(() => {
vi.clearAllMocks();
});
it('dovrebbe restituire gli elementi con successo', 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 elemento/i restituito/i',
data: [],
status: true,
});
});
it('dovrebbe generare un errore', async () => {
const mError = new Error('Impossibile recuperare le righe');
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 });
});
});
File System
Il mocking del file system garantisce che i test non dipendano dal file system reale, per rendere i test più affidabili e prevedibili. Questo isolamento aiuta a evitare effetti collaterali derivanti da test precedenti. Permette di testare condizioni di errore e casi limite che potrebbero essere difficili o impossibili da replicare con un file system reale, come problemi di permessi, situazioni di disco pieno o errori di lettura/scrittura.
Vitest non fornisce nativamente un'API per simulare il file system. Puoi usare vi.mock
per simulare il modulo fs
manualmente, ma è difficile da mantenere. Invece, ti consigliamo di usare memfs
per farlo. memfs
crea un file system in memoria, che simula le operazioni del file system senza toccare il disco reale. Questo approccio è veloce e sicuro, evitando potenziali effetti collaterali sul file system reale.
Esempio
Per reindirizzare automaticamente ogni chiamata fs
a memfs
, puoi creare i file __mocks__/fs.cjs
e __mocks__/fs/promises.cjs
nella root del tuo progetto:
// we can also use `import`, but then
// every export should be explicitly defined
const { fs } = require('memfs');
module.exports = fs;
// we can also use `import`, but then
// every export should be explicitly defined
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';
// indica a Vitest di usare il mock del modulo `fs` dalla cartella `__mocks__`
// questo può essere fatto in un file di setup se fs deve essere sempre mockato
vi.mock('node:fs');
vi.mock('node:fs/promises');
beforeEach(() => {
// resetta lo stato del file system in memoria
vol.reset();
});
it('dovrebbe restituire il testo corretto', () => {
const path = '/hello-world.txt';
fs.writeFileSync(path, 'hello world');
const text = readHelloWorld(path);
expect(text).toBe('hello world');
});
it('può restituire un valore più volte', () => {
// puoi usare vol.fromJSON per definire diversi file
vol.fromJSON(
{
'./dir1/hw.txt': 'hello dir1',
'./dir2/hw.txt': 'hello dir2',
},
// cwd predefinito
'/tmp'
);
expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1');
expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2');
});
Richieste
Poiché Vitest viene eseguito in Node, il mocking delle richieste di rete è complesso; le API web non sono disponibili, quindi abbiamo bisogno di qualcosa che simuli il comportamento di rete per noi. Consigliamo Mock Service Worker per raggiungere questo obiettivo. Ti permetterà di simulare sia le richieste di rete REST
che GraphQL
, ed è indipendente dal framework.
Mock Service Worker (MSW) funziona intercettando le richieste che i tuoi test eseguono, permettendoti di usarlo senza modificare il codice della tua applicazione. Nel browser, questo utilizza l'API Service Worker. In Node.js, e per Vitest, utilizza la libreria @mswjs/interceptors
. Per saperne di più su MSW, leggi la loro introduzione
Configurazione
Puoi usarlo come segue nel tuo file di setup
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);
// Avvia il server prima di tutti i test
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Chiudi il server dopo tutti i test
afterAll(() => server.close());
// Resetta i gestori dopo ogni test `important for test isolation`
afterEach(() => server.resetHandlers());
La configurazione del server con
onUnhandledRequest: 'error'
garantisce che venga generato un errore ogni volta che c'è una richiesta che non ha un gestore di richieste corrispondente.
Altro
C'è molto di più in MSW. Puoi accedere a cookie e parametri di query, definire risposte di errore simulate, e molto altro! Per vedere tutto ciò che puoi fare con MSW, leggi la loro documentazione.
Timer
Quando testiamo codice che coinvolge timeout o intervalli, invece di far attendere i nostri test o farli andare in timeout, possiamo accelerarli usando timer "falsi" che simulano le chiamate a setTimeout
e setInterval
.
Consulta la sezione API di vi.useFakeTimers
per una descrizione API più dettagliata.
Esempio
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2); // 2 ore
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60); // 1 minuto
}
const mock = vi.fn(() => console.log('eseguito'));
describe('esecuzione ritardata', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllMocks();
});
it('dovrebbe eseguire la funzione', () => {
executeAfterTwoHours(mock);
vi.runAllTimers();
expect(mock).toHaveBeenCalledTimes(1);
});
it('non dovrebbe eseguire la funzione', () => {
executeAfterTwoHours(mock);
// avanzare di 2ms non attiverà la funzione
vi.advanceTimersByTime(2);
expect(mock).not.toHaveBeenCalled();
});
it('dovrebbe eseguire ogni minuto', () => {
executeEveryMinute(mock);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(1);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(2);
});
});
Classi
Puoi simulare un'intera classe con una singola chiamata vi.fn
- poiché tutte le classi sono anche funzioni, questo funziona out of the box. Si noti che attualmente Vitest non rispetta la parola chiave new
, quindi new.target
è sempre undefined
nel corpo di una funzione.
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
static getType(): string {
return 'animal';
}
speak(): string {
return 'bark!';
}
isHungry() {}
feed() {}
}
Possiamo implementare questa classe con funzioni ES5:
const Dog = vi.fn(function (name) {
this.name = name;
});
// si noti che i metodi statici sono mockati direttamente sulla funzione,
// non sull'istanza della classe
Dog.getType = vi.fn(() => 'mocked animal');
// simula i metodi "speak" e "feed" su ogni istanza di una classe
// tutte le istanze `new Dog()` erediteranno queste spy
Dog.prototype.speak = vi.fn(() => 'loud bark!');
Dog.prototype.feed = vi.fn();
QUANDO USARLO?
In generale, si ricreerebbe una classe in questo modo all'interno della factory del modulo se la classe viene riesportata da un altro modulo:
import { Dog } from './dog.js';
vi.mock(import('./dog.js'), () => {
const Dog = vi.fn();
Dog.prototype.feed = vi.fn();
// ... altri mock
return { Dog };
});
Questo metodo può essere utilizzato anche per passare un'istanza di una classe a una funzione che implementa la stessa interfaccia:
// ./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('può nutrire i cani', () => {
const dogMax = new Dog('Max');
feed(dogMax);
expect(dogMax.feed).toHaveBeenCalled();
expect(dogMax.isHungry()).toBe(false);
});
Ora, quando creiamo una nuova istanza della classe Dog
, il suo metodo speak
(insieme a feed
) è già mockato:
const dog = new Dog('Cooper');
dog.speak(); // abbaio forte!
// puoi usare le asserzioni integrate per verificare la validità della chiamata
expect(dog.speak).toHaveBeenCalled();
Possiamo riassegnare il valore di ritorno per un'istanza specifica:
const dog = new Dog('Cooper');
// "vi.mocked" è un helper di tipo, poiché
// TypeScript non sa che Dog è una classe mockata,
// avvolge qualsiasi funzione in un tipo MockInstance<T>
// senza convalidare se la funzione è un mock
vi.mocked(dog.speak).mockReturnValue('woof woof');
dog.speak(); // woof woof
Per simulare la proprietà, possiamo usare il metodo vi.spyOn(dog, 'name', 'get')
. Questo rende possibile usare le asserzioni spy sulla proprietà simulata:
const dog = new Dog('Cooper');
const nameSpy = vi.spyOn(dog, 'name', 'get').mockReturnValue('Max');
expect(dog.name).toBe('Max');
expect(nameSpy).toHaveBeenCalledTimes(1);
TIP
Puoi anche monitorare getter e setter usando lo stesso metodo.
Foglio riassuntivo
INFO
vi
negli esempi seguenti è importato direttamente da vitest
. Puoi anche usarlo globalmente, se imposti globals
a true
nella tua configurazione.
Per…
Simulare variabili esportate
// 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');
Simulare una funzione esportata
- Esempio con
vi.mock
:
WARNING
Non dimenticare che una chiamata vi.mock
viene processata all'inizio del file. Verrà sempre eseguita prima di tutte le importazioni.
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
- Esempio con
vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
Simulare l'implementazione di una classe esportata
- Esempio con
vi.mock
e.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 conterrà le istanze di SomeClass.
- Esempio con
vi.spyOn
:
import * as mod from './some-path.js';
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
vi.spyOn(mod, 'SomeClass').mockImplementation(SomeClass);
Spiare un oggetto restituito da una funzione
- Esempio usando la 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(),
};
}
// ora ogni volta che useObject() viene chiamato,
// restituirà lo stesso riferimento all'oggetto
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method è stato chiamato all'interno del modulo `some-path`
expect(obj.method).toHaveBeenCalled();
Simulare parte di un modulo
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(); // ha il comportamento originale
mocked(); // è una funzione spy
WARNING
Non dimenticare che questo simula solo l'accesso dall'esterno. In questo esempio, se original
chiama mocked
internamente, chiamerà sempre la funzione definita nel modulo, non nella factory del mock.
Simulare la data corrente
Per impostare un orario fittizio per Date
, puoi usare la funzione helper vi.setSystemTime
. Questo valore non si resetterà automaticamente tra un test e l'altro.
Attenzione che l'uso di vi.useFakeTimers
cambia anche l'ora di Date
.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// resetta il tempo mockato
vi.useRealTimers();
Simulare una variabile globale
Puoi impostare una variabile globale assegnando un valore a globalThis
o utilizzando l'helper vi.stubGlobal
. Quando usi vi.stubGlobal
, non si resetterà automaticamente tra un test e l'altro, a meno che tu non abiliti l'opzione di configurazione unstubGlobals
o chiami vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
Simulare import.meta.env
- Per modificare una variabile d'ambiente, basta assegnarle un nuovo valore.
WARNING
Il valore della variabile d'ambiente non si resetterà automaticamente tra un test e l'altro.
import { beforeEach, expect, it } from 'vitest';
// puoi resettarlo manualmente nel beforeEach hook
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('cambia valore', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
- Se vuoi ripristinare automaticamente il/i valore/i, puoi usare l'helper
vi.stubEnv
con l'opzione di configurazioneunstubEnvs
abilitata (o chiamarevi.unstubAllEnvs
manualmente in un hookbeforeEach
):
import { expect, it, vi } from 'vitest';
// Prima di eseguire i test, il valore di "VITE_ENV" è "test".
import.meta.env.VITE_ENV === 'test';
it('cambia valore', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('il valore viene ripristinato prima dell\'esecuzione di un nuovo test', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default defineConfig({
test: {
unstubEnvs: true,
},
});