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 :
- Fournir un alias
export default { test: { alias: { '$app/forms': resolve('./mocks/forms.js') } }}
- Fournir un plugin qui résout un module virtuel
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'originevi.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 :
import * as mod from './foobar.js'
vi.spyOn(mod, 'foo')
// foo exporté référence la méthode mockéemod.foobar(mod.foo)
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 todosexport 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
// nous pouvons aussi utiliser `import`, mais alors// chaque export doit être défini explicitement
const { fs } = require('memfs')module.exports = fs.promises
import { readFileSync } from 'node:fs'
export function readHelloWorld(path) { return readFileSync(path)}
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 testsbeforeAll(() => server.listen({ onUnhandledRequest: 'error' }))
// Fermer le serveur après tous les testsafterAll(() => 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
export const getter = 'variable'
import * as exports from './some-path.js'
vi.spyOn(exports, 'getter', 'get').mockReturnValue('mocké')
Mock d’une fonction exportée
- Exemple avec
vi.mock
:
export function method() {}
import { method } from './some-path.js'
vi.mock('./some-path.js', () => ({ method: vi.fn()}))
- 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
- Exemple avec
vi.mock
et.prototype
:
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
- 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é
- 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
- Exemple en utilisant le cache :
export function useObject() { return { method: () => true }}
import { useObject } from './some-path.js'
const obj = useObject()obj.method()
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-pathexpect(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 originalmocked() // 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
- 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 manuellementconst 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')})
- Si vous souhaitez réinitialiser automatiquement les valeurs, vous pouvez utiliser l’assistant
vi.stubEnv
avec l’option de configurationunstubEnvs
activée (ou appeler manuellementvi.unstubAllEnvs
dans un hookbeforeEach
) :
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')})
export default defineConfig({ test: { unstubAllEnvs: true, }})