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 saranno svuotati.
- Tutte le primitive e le collezioni rimarranno inalterate.
- Tutti gli oggetti saranno clonati in profondità.
- Tutte le istanze di classi e i loro prototipi saranno clonati in profondità.
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 node-request-interceptor. 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 { 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);
// 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
:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
Ricorda che la chiamata vi.mock
viene eseguita all'inizio del file. Non inserire chiamate vi.mock
all'interno di beforeEach
, altrimenti solo una di queste simulerà effettivamente il modulo.
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 (prototipo):
// 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 () => {
const mod = await vi.importActual<typeof import('./some-path.js')>(
'./some-path.js'
);
return {
...mod,
simulato: 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, puoi semplicemente assegnarle un nuovo valore. Questo valore non verrà automaticamente reimpostato tra test diversi.
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 il valore, puoi utilizzare l'helper vi.stubEnv
con l'opzione di configurazione unstubEnvs
abilitata (o chiamare vi.unstubAllEnvs
manualmente nell'hook beforeEach
):
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,
},
};