Simulations
Lors de l'écriture de tests, il est fréquent de devoir créer une version simulée d'un service interne ou externe. C'est ce qu'on appelle communément le mocking. Vitest fournit des utilitaires pour vous aider, grâce à son outil vi. Vous pouvez import { vi } from 'vitest'
ou y accéder globalement (lorsque la configuration globale est activée).
WARNING
N'oubliez pas de réinitialiser ou de restaurer les mocks avant ou après chaque test pour éviter les modifications d'état entre les exécutions ! Consultez la documentation de mockReset
pour plus d'informations.
Si vous souhaitez explorer directement, consultez la section API, sinon, continuez votre lecture pour une exploration plus détaillée des simulations.
Dates
Il est parfois nécessaire de maîtriser la date pour garantir la cohérence des tests. Vitest utilise le package @sinonjs/fake-timers
pour manipuler les temporisateurs, ainsi que la date système. Vous trouverez plus d'informations sur l'API spécifique ici.
Exemple
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(() => {
// Indique à Vitest que nous utilisons un temps simulé
vi.useFakeTimers();
});
afterEach(() => {
// Restaure la date après chaque exécution de test
vi.useRealTimers();
});
it('autorise les achats pendant les heures d'ouverture', () => {
// Définit l'heure pendant les heures d'ouverture
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
// L'accès à Date.now() renverra la date définie ci-dessus
expect(purchase()).toEqual({ message: 'Success' });
});
it('n\'autorise pas les achats en dehors des heures d'ouverture', () => {
// Définit l'heure en dehors des heures d'ouverture
const date = new Date(2000, 1, 1, 19);
vi.setSystemTime(date);
// L'accès à Date.now() renverra la date définie ci-dessus
expect(purchase()).toEqual({ message: 'Error' });
});
});
Fonctions
La simulation de fonctions peut être divisée en deux catégories distinctes : espionnage et mocking.
Parfois, il suffit de valider si une fonction spécifique a été appelée ou non (et éventuellement quels arguments ont été passés). Dans ce cas, un espion suffit, et vous pouvez l'utiliser directement avec vi.spyOn()
(en savoir plus ici).
Cependant, les espions ne peuvent qu'observer les fonctions, ils ne peuvent pas modifier leur implémentation. Si nous devons créer une version simulée d'une fonction, nous pouvons utiliser vi.fn()
(en savoir plus ici).
Nous utilisons Tinyspy comme base pour simuler des fonctions, mais nous avons notre propre wrapper pour le rendre compatible avec jest
. vi.fn()
et vi.spyOn()
partagent les mêmes méthodes, cependant, seul le résultat de retour de vi.fn()
est appelable.
Exemple
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, // peut également être un `getter ou setter si pris en charge`
};
describe('reading messages', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('devrait obtenir le dernier message avec un espion', () => {
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('devrait obtenir le dernier message avec 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);
});
});
En savoir plus
Variables Globales
Vous pouvez simuler des variables globales qui ne sont pas présentes avec jsdom
ou node
en utilisant l'assistant vi.stubGlobal
. Il insérera la valeur de la variable globale dans l'objet 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);
// Vous pouvez maintenant y accéder comme `IntersectionObserver` ou `window.IntersectionObserver`
Modules
Les modules simulés permettent d'observer les bibliothèques tierces, qui sont invoquées dans un autre code, afin de tester les arguments, la sortie, ou même de redéfinir leur implémentation.
Consultez la section API vi.mock()
pour une description plus approfondie de l'API.
Algorithme d'automocking
Si votre code importe un module simulé, sans aucun fichier __mocks__
associé ou factory
pour ce module, Vitest simulera le module lui-même en l'invoquant et en simulant chaque exportation.
Les règles suivantes s'appliquent :
- Tous les tableaux seront réinitialisés
- Toutes les primitives et collections resteront inchangées
- Tous les objets seront clonés en profondeur
- Toutes les instances de classes et leurs prototypes seront clonées en profondeur
Exemple
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('devrait renvoyer les éléments avec succès', 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('devrait lancer une erreur', 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 });
});
});
Requêtes
La simulation des requêtes réseau est complexe car Vitest s'exécute dans Node ; les API web ne sont pas disponibles, nous avons donc besoin de quelque chose qui imitera le comportement du réseau pour nous. Nous recommandons Mock Service Worker pour cela. Il vous permettra de simuler les requêtes réseau REST
et GraphQL
, et est indépendant du framework.
Mock Service Worker (MSW) fonctionne en interceptant les requêtes que vos tests effectuent, vous permettant de l'utiliser sans modifier votre code d'application. Dans le navigateur, cela utilise l'API Service Worker. Dans Node.js, et pour Vitest, il utilise node-request-interceptor. Pour en savoir plus sur MSW, consultez leur introduction
Configuration
Vous pouvez l'utiliser comme ci-dessous dans votre fichier de configuration
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);
// Démarre le serveur avant tous les tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
// Ferme le serveur après tous les tests
afterAll(() => server.close());
// Réinitialise les gestionnaires après chaque test `important pour l'isolation des tests`
afterEach(() => server.resetHandlers());
La configuration du serveur avec
onUnhandleRequest: 'error'
garantit qu'une erreur est levée chaque fois qu'il y a une requête qui n'a pas de gestionnaire de requête correspondant.
Exemple
Nous avons un exemple de travail complet qui utilise MSW : React Testing with MSW.
Plus d'informations
Il y a beaucoup plus à découvrir avec MSW. Vous pouvez accéder aux cookies et aux paramètres de requête, définir des réponses d'erreur simulées, et bien plus encore ! Pour voir tout ce que vous pouvez faire avec MSW, consultez leur documentation.
Temporisateurs
Lorsque nous testons du code qui implique des délais d'attente ou des intervalles, au lieu de laisser nos tests attendre ou expirer, nous pouvons accélérer nos tests en utilisant des temporisateurs "factices" qui simulent les appels à setTimeout
et setInterval
.
Consultez la section API vi.useFakeTimers
pour une description plus approfondie de l'API.
Exemple
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('devrait exécuter la fonction', () => {
executeAfterTwoHours(mock);
vi.runAllTimers();
expect(mock).toHaveBeenCalledTimes(1);
});
it('ne devrait pas exécuter la fonction', () => {
executeAfterTwoHours(mock);
// L'avancement de 2ms ne déclenchera pas la fonction
vi.advanceTimersByTime(2);
expect(mock).not.toHaveBeenCalled();
});
it('devrait exécuter toutes les minutes', () => {
executeEveryMinute(mock);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(1);
vi.advanceTimersToNextTimer();
expect(mock).toHaveBeenCalledTimes(2);
});
});
Mémo
INFO
vi
dans les exemples ci-dessous est importé directement de vitest
. Vous pouvez également l'utiliser globalement si vous définissez globals
à true
dans votre configuration.
Je veux…
- Espionner une
method
const instance = new SomeClass();
vi.spyOn(instance, 'method');
- Simuler des variables exportées
// 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');
- Simuler une fonction exportée
Exemple avec vi.mock
:
// ./some-path.js
export function method() {}
import { method } from './some-path.js';
vi.mock('./some-path.js', () => ({
method: vi.fn(),
}));
WARNING
N'oubliez pas que l'appel vi.mock
est remonté (hoisted) en haut du fichier. Ne placez pas les appels vi.mock
à l'intérieur de beforeEach
: seul le premier appel simulera le module.
Exemple avec vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'method').mockImplementation(() => {});
- Simuler l'implémentation d'une classe exportée
Exemple avec vi.mock
et 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 contiendra des instances de SomeClass
Exemple avec vi.mock
et valeur de retour :
import { SomeClass } from './some-path.js';
vi.mock('./some-path.js', () => {
const SomeClass = vi.fn(() => ({
someMethod: vi.fn(),
}));
return { SomeClass };
});
// SomeClass.mock.results contiendra les objets retournés
Exemple avec vi.spyOn
:
import * as exports from './some-path.js';
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// selon ce qui vous convient parmi les exemples précédents
});
- Espionner un objet retourné par une fonction
Exemple utilisant le 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(),
};
}
// Désormais, chaque fois que `useObject()` est appelé, il
// renverra la même référence d'objet
return _cache;
};
return { useObject };
});
const obj = useObject();
// obj.method a été appelé à l'intérieur de some-path
expect(obj.method).toHaveBeenCalled();
- Simuler une partie d'un module
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,
mocked: vi.fn(),
};
});
original(); // a le comportement d'origine
mocked(); // est une fonction espion
- Simuler la date actuelle
Pour simuler l'heure de Date
, vous pouvez utiliser la fonction d'assistance vi.setSystemTime
. Cette valeur ne sera pas automatiquement réinitialisée entre les tests.
Notez que l'utilisation de vi.useFakeTimers
modifie également l'heure de Date
.
const mockDate = new Date(2022, 0, 1);
vi.setSystemTime(mockDate);
const now = new Date();
expect(now.valueOf()).toBe(mockDate.valueOf());
// réinitialiser l'heure simulée
vi.useRealTimers();
- Simuler une variable globale
Vous pouvez définir une variable globale en assignant une valeur à globalThis
ou en utilisant la fonction d'assistance vi.stubGlobal
. Lorsque vous utilisez vi.stubGlobal
, la variable ne sera pas automatiquement réinitialisée entre les tests, sauf si vous activez l'option de configuration unstubGlobals
ou si vous appelez vi.unstubAllGlobals
.
vi.stubGlobal('__VERSION__', '1.0.0');
expect(__VERSION__).toBe('1.0.0');
- Simuler
import.meta.env
Pour modifier une variable d'environnement, vous pouvez simplement lui assigner une nouvelle valeur. Cette valeur ne sera pas automatiquement réinitialisée entre les tests.
import { beforeEach, expect, it } from 'vitest';
// vous pouvez la réinitialiser manuellement dans le hook `beforeEach`
const originalViteEnv = import.meta.env.VITE_ENV;
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv;
});
it('modifie la valeur', () => {
import.meta.env.VITE_ENV = 'staging';
expect(import.meta.env.VITE_ENV).toBe('staging');
});
Si vous souhaitez réinitialiser automatiquement la valeur, vous pouvez utiliser la fonction d'assistance vi.stubEnv
avec l'option de configuration unstubEnvs
activée (ou appeler vi.unstubAllEnvs
manuellement dans le hook beforeEach
) :
import { expect, it, vi } from 'vitest';
// avant l'exécution des tests, "VITE_ENV" vaut "test"
import.meta.env.VITE_ENV === 'test';
it('modifie la valeur', () => {
vi.stubEnv('VITE_ENV', 'staging');
expect(import.meta.env.VITE_ENV).toBe('staging');
});
it('la valeur est restaurée avant l'exécution d'un autre test', () => {
expect(import.meta.env.VITE_ENV).toBe('test');
});
// vitest.config.ts
export default {
test: {
unstubAllEnvs: true,
},
};