Mocking
Quando si scrivono test, prima o poi si presenta la necessità di creare una versione "falsa" di un servizio interno o esterno. Questo processo è comunemente definito mocking. Vitest fornisce funzioni di utilità per facilitare il mocking tramite il suo helper vi. Puoi import { vi } from 'vitest'
o accedervi globalmente (quando la configurazione globale è abilitata).
WARNING
Ricorda sempre di cancellare o ripristinare i mock prima o dopo ogni esecuzione del test per annullare le modifiche allo stato del mock tra le esecuzioni! Consulta la documentazione di mockReset
per maggiori informazioni.
Se desideri approfondire subito l'argomento, consulta la sezione API; altrimenti, continua a leggere per esplorare il mondo del mocking.
Date
A volte, per garantire la coerenza durante i test, è necessario controllare la data. Vitest utilizza il pacchetto @sinonjs/fake-timers
per manipolare i timer, nonché la data di sistema. Puoi trovare ulteriori 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 di utilizzare un tempo simulato
vi.useFakeTimers();
});
afterEach(() => {
// ripristina la data dopo ogni esecuzione del test
vi.useRealTimers();
});
it('allows purchases within business hours', () => {
// 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('disallows purchases outside of business hours', () => {
// 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 (spionaggio) e mocking (simulazione).
A volte è sufficiente verificare se una funzione specifica è stata chiamata o meno (e possibilmente quali argomenti sono stati passati). In questi casi, una spia sarebbe sufficiente, e puoi utilizzarla direttamente con vi.spyOn()
(leggi di più qui).
Tuttavia, le spie possono solo aiutarti a monitorare le funzioni; non sono in grado di alterarne l'implementazione. Se invece è necessario creare una versione falsa (o mockata) di una funzione, si può usare vi.fn()
(leggi di più qui).
Usiamo Tinyspy come base per il mocking delle funzioni, ma abbiamo il nostro wrapper per renderlo compatibile con jest
. Sia vi.fn()
che vi.spyOn()
condividono gli stessi metodi, tuttavia solo il valore restituito da vi.fn()
è richiamabile.
Esempio
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);
});
});
Altro
Variabili Globali
È possibile mockare variabili globali non presenti in jsdom
o node
usando l'helper vi.stubGlobal
. Questo inserirà il valore della variabile globale in un 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 mockati consentono di intercettare le chiamate a librerie di terze parti nel codice, permettendo di testare argomenti, output o persino di ridefinirne l'implementazione.
Consulta la sezione API vi.mock()
per una descrizione API più approfondita e dettagliata.
Algoritmo di Automocking
Se il codice importa un modulo mock senza alcun file __mocks__
associato o factory
, Vitest mockerà il modulo stesso invocandolo e mockando ogni esportazione.
Si applicano i seguenti principi:
- Tutti gli array verranno svuotati
- Tutti i tipi primitivi e le collections rimarranno invariati
- Tutti gli oggetti verranno clonati in profondità
- Tutte le istanze delle classi e i loro prototipi verranno clonati in profondità
Moduli Virtuali
Vitest supporta il mocking dei moduli virtuali di Vite. Funziona in modo diverso rispetto a come i moduli virtuali vengono trattati in Jest. Invece di passare virtual: true
a una funzione vi.mock
, devi dire a Vite che il modulo esiste, altrimenti l'analisi fallirà. 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 punti di ingresso virtuali. Se reindirizzi più moduli virtuali in un singolo file, allora tutti saranno interessati da vi.mock
, quindi assicurati di utilizzare identificatori univoci.
Insidie del Mocking
Fai attenzione al fatto che non è possibile mockare le chiamate ai 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 mockare il metodo foo
dall'esterno perché viene referenziato direttamente. Quindi questo codice non avrà alcun effetto sulla chiamata foo
all'interno di foobar
(ma influenzerà la chiamata foo
in altri moduli):
import { vi } from 'vitest';
import * as mod from './foobar.js';
// this will only affect "foo" outside of the original module
vi.spyOn(mod, 'foo');
vi.mock('./foobar.js', async importOriginal => {
return {
...(await importOriginal<typeof import('./foobar.js')>()),
// this will only affect "foo" outside of the original module
foo: () => 'mocked',
};
});
Puoi confermare questo comportamento fornendo l'implementazione direttamente al metodo foobar
:
// foobar.test.js
import * as mod from './foobar.js';
vi.spyOn(mod, 'foo');
// exported foo references mocked method
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 è segno di codice errato 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 delle dipendenze.
Esempio
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 });
});
});
Richieste
Poiché Vitest viene eseguito in Node, mockare le richieste di rete è complesso: le API web non sono disponibili, quindi è necessario qualcosa che ne imiti il comportamento. Si consiglia di utilizzare Mock Service Worker per questo scopo. Questo permette di simulare sia le richieste di rete REST
che GraphQL
, ed è indipendente dal framework.
Mock Service Worker (MSW) intercetta le richieste effettuate dai test, permettendo di usarlo senza modificare il codice dell'applicazione. Nel browser, MSW utilizza la Service Worker API. In Node.js, e per Vitest, MSW utilizza @mswjs/interceptors
. Per saperne di più su MSW, leggi la loro introduzione
Configurazione
È possibile utilizzarlo come segue nel file di setup:
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);
// Avvia il server prima di tutti i test
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Chiudi il server dopo tutti i test
afterAll(() => server.close());
// Reimposta gli handler dopo ogni test (importante per l'isolamento dei test)
afterEach(() => server.resetHandlers());
Configurare il server con
onUnhandleRequest: 'error'
assicura che venga generato un errore ogni volta che c'è una richiesta che non ha un gestore di richieste corrispondente.
Esempio
Ecco un esempio funzionante completo che utilizza MSW: React Testing with MSW.
Altro
MSW offre molte altre funzionalità. Puoi gestire i cookie e i 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 si testa codice che coinvolge timeout o intervalli, invece di attendere o far scadere i test, è possibile velocizzarli usando timer "falsi" che simulano le chiamate a setTimeout
e setInterval
.
Consulta la sezione API vi.useFakeTimers
per una descrizione più dettagliata dell'API.
Esempio
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);
// avanzando di 2ms non si attiverà la funzione
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
negli esempi seguenti viene importato direttamente da vitest
. Puoi anche usarlo globalmente, se imposti globals
su true
nella tua configurazione.
Voglio…
Spiare un metodo
const instance = new SomeClass();
vi.spyOn(instance, 'method');
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 a vi.mock
viene sollevata all'inizio del file. Sarà sempre eseguita prima di tutti gli import.
// ./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('./some-path.js', () => {
const SomeClass = vi.fn();
SomeClass.prototype.someMethod = vi.fn();
return { SomeClass };
});
// SomeClass.mock.instances conterrà SomeClass
- Esempio con
vi.mock
e valore di ritorno:
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// SomeClass.mock.returns conterrà l'oggetto restituito
- Esempio con
vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// qualsiasi implementazione dei primi due esempi
});
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('./some-path.js', () => {
let _cache;
const useObject = () => {
if (!_cache) {
_cache = {
method: vi.fn(),
};
}
// ora ogni volta che useObject() viene chiamata,
// restituirà lo stesso riferimento all'oggetto
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method è stata chiamata all'interno di some-path
expect(obj.method).toHaveBeenCalled();
Simulare una parte di un modulo
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(); // ha il comportamento originale
mocked(); // è una funzione spia
Simulare la data corrente
Per simulare l'ora di Date
, puoi usare la funzione helper vi.setSystemTime
. Questo valore non verrà automaticamente reimpostato tra test diversi.
Tieni presente che l'utilizzo di vi.useFakeTimers
modifica 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());
// ripristina l'ora simulata
vi.useRealTimers();
Simulare una variabile globale
Puoi impostare una variabile globale assegnando un valore a globalThis
o usando l'helper vi.stubGlobal
. Quando si utilizza vi.stubGlobal
, questo non verrà automaticamente reimpostato tra test diversi, a meno che non si abiliti l'opzione di configurazione unstubGlobals
o si 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, è sufficiente assegnarle un nuovo valore.
WARNING
Il valore della variabile d'ambiente non verrà ripristinato automaticamente tra i diversi test.
import { beforeEach, expect, it } from 'vitest';
// puoi ripristinarlo manualmente nell'hook beforeEach
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('modifica il valore', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
- Se desideri reimpostare automaticamente i valori, puoi utilizzare l'helper
vi.stubEnv
con l'opzione di configurazioneunstubEnvs
abilitata (oppure chiamarevi.unstubAllEnvs
](/api/vi#vi-unstuballenvs) manualmente in un hookbeforeEach
):
import { expect, it, vi } from 'vitest';
// prima di eseguire i test "VITE_ENV" è "test"
import.meta.env.VITE_ENV === 'test';
it('modifica il valore', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('il valore viene ripristinato prima di eseguire un altro test', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};