Passer au contenu

Mocking

Lorsque vous écrivez des tests, il ne faut pas longtemps avant que vous ayez besoin de créer une version “fausse” d’un service interne — ou externe. Cela est communément appelé mocking. Vitest fournit des fonctions utilitaires pour vous aider à travers son assistant vi. Vous pouvez import { vi } from 'vitest' ou y accéder globalement (lorsque la configuration globale est activée).

Si vous souhaitez plonger directement, consultez la section API, sinon continuez à lire pour plonger plus profondément dans le monde du mocking.

Dates

Parfois, vous devez contrôler la date pour garantir la cohérence lors des tests. Vitest utilise le package @sinonjs/fake-timers pour manipuler les temporisateurs, ainsi que la date système. Vous pouvez en savoir plus sur l’API spécifique en détail 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('flux d\'achat', () => {
beforeEach(() => {
// indiquer à vitest que nous utilisons le temps mocké
vi.useFakeTimers()
})
afterEach(() => {
// restaurer la date après chaque exécution de test
vi.useRealTimers()
})
it('autorise les achats pendant les heures d\'ouverture', () => {
// définir l'heure pendant les heures d'ouverture
const date = new Date(2000, 1, 1, 13)
vi.setSystemTime(date)
// accéder à 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éfinir l'heure en dehors des heures d'ouverture
const date = new Date(2000, 1, 1, 19)
vi.setSystemTime(date)
// accéder à Date.now() renverra la date définie ci-dessus
expect(purchase()).toEqual({ message: 'Error' })
})
})

Fonctions

Le mocking des fonctions peut être divisé en deux catégories différentes : espionnage & mocking.

Parfois, tout ce dont vous avez besoin est de valider si une fonction spécifique a été appelée (et éventuellement quels arguments ont été passés). Dans ces cas, un espion serait tout ce dont nous avons besoin, que vous pouvez utiliser directement avec vi.spyOn() (lisez-en plus ici).

Cependant, les espions ne peuvent que vous espionner sur des fonctions, ils ne peuvent pas modifier l’implémentation de ces fonctions. Dans le cas où nous devons créer une version fausse (ou mockée) d’une fonction, nous pouvons utiliser vi.fn() (lisez-en plus ici).

Nous utilisons Tinyspy comme base pour le mocking des fonctions, mais nous avons notre propre wrapper pour le rendre compatible avec jest. Les deux 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'
const messages = {
items: [
{ message: 'Message de test simple', from: 'Testman' },
// ...
],
getLatest, // peut aussi être un `getter ou setter si supporté`
}
function getLatest(index = messages.items.length - 1) {
return messages.items[index]
}
describe('lecture des 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(() => 'accès interdit')
expect(messages.getLatest()).toEqual('accès interdit')
expect(spy).toHaveBeenCalledTimes(2)
})
it('devrait obtenir avec un mock', () => {
const mock = vi.fn().mockImplementation(getLatest)
expect(mock()).toEqual(messages.items[messages.items.length - 1])
expect(mock).toHaveBeenCalledTimes(1)
mock.mockImplementationOnce(() => 'accès interdit')
expect(mock()).toEqual('accès interdit')
expect(mock).toHaveBeenCalledTimes(2)
expect(mock()).toEqual(messages.items[messages.items.length - 1])
expect(mock).toHaveBeenCalledTimes(3)
})
})

Plus

Globals

Vous pouvez mocker des variables globales qui ne sont pas présentes avec jsdom ou node en utilisant l’assistant vi.stubGlobal. Cela mettra la valeur de la variable globale dans un 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)
// maintenant vous pouvez y accéder comme `IntersectionObserver` ou `window.IntersectionObserver`

Modules

Le mocking des modules observe des bibliothèques tiers, qui sont invoquées dans un autre code, vous permettant de tester des arguments, des sorties ou même de redéclarer son implémentation.

Consultez la section vi.mock() API pour une description plus détaillée de l’API.

Algorithme d’automocking

Si votre code importe un module mocké, sans aucun fichier associé __mocks__ ou factory pour ce module, Vitest mockera le module lui-même en l’invoquant et en mockant chaque export.

Les principes suivants s’appliquent :

  • Tous les tableaux seront vidés
  • Tous les primitifs et les collections resteront les mêmes
  • Tous les objets seront clonés en profondeur
  • Toutes les instances de classes et leurs prototypes seront clonés en profondeur

Modules Virtuels

Vitest prend en charge le mocking des modules virtuels Vite. Il fonctionne différemment de la façon dont les modules virtuels sont traités dans Jest. Au lieu de transmettre virtual: true à une fonction vi.mock, vous devez dire à Vite que le module existe sinon cela échouera lors de l’analyse. Vous pouvez le faire de plusieurs manières :

  1. Fournir un alias
vitest.config.js
export default {
test: {
alias: {
'$app/forms': resolve('./mocks/forms.js')
}
}
}
  1. Fournir un plugin qui résout un module virtuel
vitest.config.js
export default {
plugins: [
{
name: 'virtual-modules',
resolveId(id) {
if (id === '$app/forms') {
return 'virtual:$app/forms'
}
}
}
]
}

L’avantage de la deuxième approche est que vous pouvez créer dynamiquement différents points d’entrée virtuels. Si vous redirigez plusieurs modules virtuels vers un seul fichier, alors tous seront affectés par vi.mock, alors assurez-vous d’utiliser des identifiants uniques.

Pièges du Mocking

Attention, il n’est pas possible de mocker les appels à des méthodes qui sont appelées à l’intérieur d’autres méthodes du même fichier. Par exemple, dans ce code :

export function foo() {
return 'foo'
}
export function foobar() {
return `${foo()}bar`
}

Il n’est pas possible de mocker la méthode foo de l’extérieur car elle est référencée directement. Donc ce code n’aura aucun effet sur l’appel foo à l’intérieur de foobar (mais il affectera l’appel foo dans d’autres modules) :

import { vi } from 'vitest'
import * as mod from './foobar.js'
// cela n'affectera que "foo" à l'extérieur du module d'origine
vi.spyOn(mod, 'foo')
vi.mock('./foobar.js', async (importOriginal) => {
return {
...await importOriginal<typeof import('./foobar.js')>(),
// cela n'affectera que "foo" à l'extérieur du module d'origine
foo: () => 'mocké'
}
})

Vous pouvez confirmer ce comportement en fournissant l’implémentation à la méthode foobar directement :

foobar.test.js
import * as mod from './foobar.js'
vi.spyOn(mod, 'foo')
// foo exporté référence la méthode mockée
mod.foobar(mod.foo)
foobar.js
export function foo() {
return 'foo'
}
export function foobar(injectedFoo) {
return injectedFoo === foo // faux
}

C’est le comportement prévu. C’est généralement un signe de mauvais code lorsque le mocking est impliqué de cette manière. Envisagez de refactoriser votre code en plusieurs fichiers ou d’améliorer l’architecture de votre application en utilisant des techniques telles que l’injection de dépendance.

Exemple

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { Client } from 'pg'
import { failure, success } from './handlers.js'
// obtenir les 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('obtenir une liste d\'éléments todo', () => {
let client
beforeEach(() => {
client = new Client()
})
afterEach(() => {
vi.clearAllMocks()
})
it('devrait retourner des é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 générer une erreur', async () => {
const mError = new Error('Impossible de récupérer des lignes')
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 })
})
})

Système de Fichiers

Le mocking du système de fichiers garantit que les tests ne dépendent pas du système de fichiers réel, rendant les tests plus fiables et prévisibles. Cette isolation aide à éviter les effets secondaires des tests précédents. Elle permet de tester des conditions d’erreur et des cas limites qui pourraient être difficiles ou impossibles à reproduire avec un système de fichiers réel, tels que des problèmes de permission, des scénarios de disque plein ou des erreurs de lecture/écriture.

Vitest ne fournit aucune API de mocking du système de fichiers par défaut. Vous pouvez utiliser vi.mock pour moquer manuellement le module fs, mais c’est difficile à maintenir. Au lieu de cela, nous recommandons d’utiliser memfs pour le faire pour vous. memfs crée un système de fichiers en mémoire, qui simule les opérations du système de fichiers sans toucher au disque réel. Cette approche est rapide et sûre, évitant tout potentiel effet secondaire sur le système de fichiers réel.

Exemple

Pour rediriger automatiquement chaque appel fs vers memfs, vous pouvez créer les fichiers __mocks__/fs.cjs et __mocks__/fs/promises.cjs à la racine de votre projet :

// nous pouvons aussi utiliser `import`, mais alors
// chaque export doit être défini explicitement
const { fs } = require('memfs')
module.exports = fs
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'
// dire à vitest d'utiliser le mock fs depuis le dossier __mocks__
// cela peut être fait dans un fichier de configuration si fs doit toujours être moqué
vi.mock('node:fs')
vi.mock('node:fs/promises')
beforeEach(() => {
// réinitialiser l'état du fs en mémoire
vol.reset()
})
it('devrait retourner le texte correct', () => {
const path = '/hello-world.txt'
fs.writeFileSync(path, 'hello world')
const text = readHelloWorld(path)
expect(text).toBe('hello world')
})
it('peut retourner une valeur plusieurs fois', () => {
// vous pouvez utiliser vol.fromJSON pour définir plusieurs fichiers
vol.fromJSON(
{
'./dir1/hw.txt': 'hello dir1',
'./dir2/hw.txt': 'hello dir2',
},
// cwd par défaut
'/tmp',
)
expect(readHelloWorld('/tmp/dir1/hw.txt')).toBe('hello dir1')
expect(readHelloWorld('/tmp/dir2/hw.txt')).toBe('hello dir2')
})

Requêtes

Parce que Vitest s’exécute dans Node, le mocking des requêtes réseau est délicat ; les API Web ne sont pas disponibles, donc nous avons besoin de quelque chose qui imite le comportement du réseau pour nous. Nous recommandons Mock Service Worker pour accomplir cela. Il vous permettra de mocker à la fois les requêtes réseau REST et GraphQL, et est indépendant des frameworks.

Mock Service Worker (MSW) fonctionne en interceptant les requêtes que vos tests font, vous permettant de l’utiliser sans changer aucun de votre code d’application. Dans le navigateur, cela utilise l’API Service Worker. Dans Node.js, et pour Vitest, cela utilise la bibliothèque @mswjs/interceptors. Pour en savoir plus sur MSW, lisez 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 { HttpResponse, graphql, http } from 'msw'
const posts = [
{
userId: 1,
id: 1,
title: 'titre du premier post',
body: 'corps du premier post',
},
// ...
]
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)
// Démarrer le serveur avant tous les tests
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// Fermer le serveur après tous les tests
afterAll(() => server.close())
// Réinitialiser les gestionnaires après chaque test `important pour l'isolation des tests`
afterEach(() => server.resetHandlers())

Configurer le serveur avec onUnhandleRequest: 'error' garantit qu’une erreur est lancée chaque fois qu’il y a une requête qui n’a pas de gestionnaire de requête correspondant.

Plus

Il y a beaucoup plus à MSW. Vous pouvez accéder aux cookies et aux paramètres de requête, définir des réponses d’erreur fictives, et bien plus encore ! Pour voir tout ce que vous pouvez faire avec MSW, lisez leur documentation.

Temporisateurs

Lorsque nous testons du code qui implique des délais ou des intervalles, au lieu de faire attendre nos tests ou de provoquer un délai d’attente, nous pouvons accélérer nos tests en utilisant des temporisateurs “faux” qui moquent les appels à setTimeout et setInterval.

Consultez la section vi.useFakeTimers API pour une description plus détaillée de l’API.

Exemple

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
function executeAfterTwoHours(func) {
setTimeout(func, 1000 * 60 * 60 * 2) // 2 heures
}
function executeEveryMinute(func) {
setInterval(func, 1000 * 60) // 1 minute
}
const mock = vi.fn(() => console.log('exécuté'))
describe('exécution différée', () => {
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)
// avancer 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)
})
})

Fiche de Révision

vi dans les exemples ci-dessous est importé directement depuis vitest. Vous pouvez également l’utiliser globalement, si vous définissez globals sur true dans votre config.

Je veux…

Espionner une méthode

const instance = new SomeClass()
vi.spyOn(instance, 'method')

Mock 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('mocké')

Mock d’une fonction exportée

  1. Exemple avec vi.mock :
./some-path.js
export function method() {}
import { method } from './some-path.js'
vi.mock('./some-path.js', () => ({
method: vi.fn()
}))
  1. Exemple avec vi.spyOn :
import * as exports from './some-path.js'
vi.spyOn(exports, 'method').mockImplementation(() => {})

Mock d’une implémentation de classe exportée

  1. 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 SomeClass
  1. Exemple avec vi.mock et une 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.returns contiendra l'objet retourné
  1. Exemple avec vi.spyOn:
import * as exports from './some-path.js'
vi.spyOn(exports, 'SomeClass').mockImplementation(() => {
// quoi que ce soit qui convienne des deux premiers exemples
})

Espionner un objet retourné par une fonction

  1. Exemple en 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(),
}
}
// maintenant 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()

Mock d’une partie d’un module

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() // a un comportement original
mocked() // est une fonction espion

Mock de la date actuelle

Pour simuler le temps de Date, vous pouvez utiliser la fonction d’assistance vi.setSystemTime. Cette valeur ne se réinitialise pas automatiquement entre les différents tests.

Attention, l’utilisation de vi.useFakeTimers change également le temps de Date.

const mockDate = new Date(2022, 0, 1)
vi.setSystemTime(mockDate)
const now = new Date()
expect(now.valueOf()).toBe(mockDate.valueOf())
// réinitialiser le temps mocké
vi.useRealTimers()

Mock d’une variable globale

Vous pouvez définir une variable globale en assignant une valeur à globalThis ou en utilisant l’assistant vi.stubGlobal. Lorsque vous utilisez vi.stubGlobal, elle ne se réinitialise pas automatiquement entre les différents tests, sauf si vous activez l’option de configuration unstubGlobals ou appelez vi.unstubAllGlobals.

vi.stubGlobal('__VERSION__', '1.0.0')
expect(__VERSION__).toBe('1.0.0')

Mock de import.meta.env

  1. Pour changer la variable d’environnement, vous pouvez simplement lui assigner une nouvelle valeur.
import { beforeEach, expect, it } from 'vitest'
// vous pouvez le réinitialiser dans le hook beforeEach manuellement
const originalViteEnv = import.meta.env.VITE_ENV
beforeEach(() => {
import.meta.env.VITE_ENV = originalViteEnv
})
it('change la valeur', () => {
import.meta.env.VITE_ENV = 'staging'
expect(import.meta.env.VITE_ENV).toBe('staging')
})
  1. Si vous souhaitez réinitialiser automatiquement les valeurs, vous pouvez utiliser l’assistant vi.stubEnv avec l’option de configuration unstubEnvs activée (ou appeler manuellement vi.unstubAllEnvs dans un hook beforeEach) :
import { expect, it, vi } from 'vitest'
// avant d'exécuter des tests, "VITE_ENV" est "test"
import.meta.env.VITE_ENV === 'test'
it('change la valeur', () => {
vi.stubEnv('VITE_ENV', 'staging')
expect(import.meta.env.VITE_ENV).toBe('staging')
})
it('la valeur est restaurée avant d\'exécuter un autre test', () => {
expect(import.meta.env.VITE_ENV).toBe('test')
})
vitest.config.ts
export default defineConfig({
test: {
unstubAllEnvs: true,
}
})