Le mocking est essentiel pour les tests car il vous permet de remplacer des dépendances par des implémentations contrôlées. Bun fournit des fonctionnalités de mocking complètes incluant les mocks de fonction, les spies et les mocks de module.
Mocks de fonction de base
Créez des mocks avec la fonction mock.
import { test, expect, mock } from "bun:test";
const random = mock(() => Math.random());
test("random", () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});Compatibilité Jest
Alternativement, vous pouvez utiliser la fonction jest.fn(), comme dans Jest. Elle se comporte de manière identique.
import { test, expect, jest } from "bun:test";
const random = jest.fn(() => Math.random());
test("random", () => {
const val = random();
expect(val).toBeGreaterThan(0);
expect(random).toHaveBeenCalled();
expect(random).toHaveBeenCalledTimes(1);
});Propriétés des fonctions mock
Le résultat de mock() est une nouvelle fonction qui a été décorée avec des propriétés supplémentaires.
import { mock } from "bun:test";
const random = mock((multiplier: number) => multiplier * Math.random());
random(2);
random(10);
random.mock.calls;
// [[ 2 ], [ 10 ]]
random.mock.results;
// [
// { type: "return", value: 0.6533907460954099 },
// { type: "return", value: 0.6452713933037312 }
// ]Propriétés et méthodes disponibles
Les propriétés et méthodes suivantes sont implémentées sur les fonctions mock :
| Propriété/Méthode | Description |
|---|---|
mockFn.getMockName() | Retourne le nom du mock |
mockFn.mock.calls | Tableau des arguments d'appel pour chaque invocation |
mockFn.mock.results | Tableau des valeurs de retour pour chaque invocation |
mockFn.mock.instances | Tableau des contextes this pour chaque invocation |
mockFn.mock.contexts | Tableau des contextes this pour chaque invocation |
mockFn.mock.lastCall | Arguments de l'appel le plus récent |
mockFn.mockClear() | Efface l'historique des appels |
mockFn.mockReset() | Efface l'historique des appels et supprime l'implémentation |
mockFn.mockRestore() | Restaure l'implémentation originale |
mockFn.mockImplementation(fn) | Définit une nouvelle implémentation |
mockFn.mockImplementationOnce(fn) | Définit l'implémentation pour le prochain appel seulement |
mockFn.mockName(name) | Définit le nom du mock |
mockFn.mockReturnThis() | Définit la valeur de retour sur this |
mockFn.mockReturnValue(value) | Définit une valeur de retour |
mockFn.mockReturnValueOnce(value) | Définit la valeur de retour pour le prochain appel seulement |
mockFn.mockResolvedValue(value) | Définit une valeur de Promise résolue |
mockFn.mockResolvedValueOnce(value) | Définit la valeur résolue pour le prochain appel seulement |
mockFn.mockRejectedValue(value) | Définit une valeur de Promise rejetée |
mockFn.mockRejectedValueOnce(value) | Définit la valeur rejetée pour le prochain appel seulement |
mockFn.withImplementation(fn, callback) | Change temporairement l'implémentation |
Exemples pratiques
Utilisation de base des mocks
import { test, expect, mock } from "bun:test";
test("comportement de la fonction mock", () => {
const mockFn = mock((x: number) => x * 2);
// Appeler le mock
const result1 = mockFn(5);
const result2 = mockFn(10);
// Vérifier les appels
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn).toHaveBeenLastCalledWith(10);
// Vérifier les résultats
expect(result1).toBe(10);
expect(result2).toBe(20);
// Inspecter l'historique des appels
expect(mockFn.mock.calls).toEqual([[5], [10]]);
expect(mockFn.mock.results).toEqual([
{ type: "return", value: 10 },
{ type: "return", value: 20 },
]);
});Implémentations de mocks dynamiques
import { test, expect, mock } from "bun:test";
test("implémentations de mocks dynamiques", () => {
const mockFn = mock();
// Définir différentes implémentations
mockFn.mockImplementationOnce(() => "first");
mockFn.mockImplementationOnce(() => "second");
mockFn.mockImplementation(() => "default");
expect(mockFn()).toBe("first");
expect(mockFn()).toBe("second");
expect(mockFn()).toBe("default");
expect(mockFn()).toBe("default"); // Utilise l'implémentation par défaut
});Mocks async
import { test, expect, mock } from "bun:test";
test("fonctions mock async", async () => {
const asyncMock = mock();
// Mock des valeurs résolues
asyncMock.mockResolvedValueOnce("first result");
asyncMock.mockResolvedValue("default result");
expect(await asyncMock()).toBe("first result");
expect(await asyncMock()).toBe("default result");
// Mock des valeurs rejetées
const rejectMock = mock();
rejectMock.mockRejectedValue(new Error("Erreur mock"));
await expect(rejectMock()).rejects.toThrow("Erreur mock");
});Spies avec spyOn()
Il est possible de suivre les appels à une fonction sans la remplacer par un mock. Utilisez spyOn() pour créer un spy ; ces spies peuvent être passés à .toHaveBeenCalled() et .toHaveBeenCalledTimes().
import { test, expect, spyOn } from "bun:test";
const ringo = {
name: "Ringo",
sayHi() {
console.log(`Hello I'm ${this.name}`);
},
};
const spy = spyOn(ringo, "sayHi");
test("spyon", () => {
expect(spy).toHaveBeenCalledTimes(0);
ringo.sayHi();
expect(spy).toHaveBeenCalledTimes(1);
});Utilisation avancée des spies
import { test, expect, spyOn, afterEach } from "bun:test";
class UserService {
async getUser(id: string) {
// Implémentation originale
return { id, name: `User ${id}` };
}
async saveUser(user: any) {
// Implémentation originale
return { ...user, saved: true };
}
}
const userService = new UserService();
afterEach(() => {
// Restaurer tous les spies après chaque test
jest.restoreAllMocks();
});
test("spy sur les méthodes de service", async () => {
// Spy sans changer l'implémentation
const getUserSpy = spyOn(userService, "getUser");
const saveUserSpy = spyOn(userService, "saveUser");
// Utiliser le service normalement
const user = await userService.getUser("123");
await userService.saveUser(user);
// Vérifier les appels
expect(getUserSpy).toHaveBeenCalledWith("123");
expect(saveUserSpy).toHaveBeenCalledWith(user);
});
test("spy avec implémentation mock", async () => {
// Spy et remplacer l'implémentation
const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
id: "123",
name: "Mocked User",
});
const result = await userService.getUser("123");
expect(result.name).toBe("Mocked User");
expect(getUserSpy).toHaveBeenCalledWith("123");
});Mocks de module avec mock.module()
Le mock de module vous permet de remplacer le comportement d'un module. Utilisez mock.module(path: string, callback: () => Object) pour mocker un module.
import { test, expect, mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});
test("mock.module", async () => {
const esm = await import("./module");
expect(esm.foo).toBe("bar");
const cjs = require("./module");
expect(cjs.foo).toBe("bar");
});Comme le reste de Bun, les mocks de module prennent en charge à la fois import et require.
Remplacer des modules déjà importés
Si vous devez remplacer un module qui a déjà été importé, il n'y a rien de spécial à faire. Appelez simplement mock.module() et le module sera remplacé.
import { test, expect, mock } from "bun:test";
// Le module que nous allons mocker est ici :
import { foo } from "./module";
test("mock.module", async () => {
const cjs = require("./module");
expect(foo).toBe("bar");
expect(cjs.foo).toBe("bar");
// Nous le mettons à jour ici :
mock.module("./module", () => {
return {
foo: "baz",
};
});
// Et les liaisons live sont mises à jour.
expect(foo).toBe("baz");
// Le module est également mis à jour pour CJS.
expect(cjs.foo).toBe("baz");
});Hoisting et préchargement
Si vous devez vous assurer qu'un module est mocké avant d'être importé, vous devriez utiliser --preload pour charger vos mocks avant l'exécution de vos tests.
import { mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});bun test --preload ./my-preloadPour vous faciliter la vie, vous pouvez mettre le preload dans votre bunfig.toml :
[test]
# Charger ces modules avant d'exécuter les tests.
preload = ["./my-preload"]Bonnes pratiques pour les mocks de module
Quand utiliser le preload
Que se passe-t-il si je mock un module qui a déjà été importé ?
Si vous mockez un module qui a déjà été importé, le module sera mis à jour dans le cache de module. Cela signifie que tous les modules qui importent le module obtiendront la version mockée, MAIS le module original aura toujours été évalué. Cela signifie que tous les effets de bord du module original se seront toujours produits.
Si vous voulez empêcher l'évaluation du module original, vous devriez utiliser --preload pour charger vos mocks avant l'exécution de vos tests.
Exemples pratiques de mocks de module
import { test, expect, mock, beforeEach } from "bun:test";
// Mocker le module du client API
mock.module("./api-client", () => ({
fetchUser: mock(async (id: string) => ({ id, name: `User ${id}` })),
createUser: mock(async (user: any) => ({ ...user, id: "new-id" })),
updateUser: mock(async (id: string, user: any) => ({ ...user, id })),
}));
test("service utilisateur avec API mockée", async () => {
const { fetchUser } = await import("./api-client");
const { UserService } = await import("./user-service");
const userService = new UserService();
const user = await userService.getUser("123");
expect(fetchUser).toHaveBeenCalledWith("123");
expect(user.name).toBe("User 123");
});Mocker des dépendances externes
import { test, expect, mock } from "bun:test";
// Mocker la bibliothèque de base de données externe
mock.module("pg", () => ({
Client: mock(function () {
return {
connect: mock(async () => {}),
query: mock(async (sql: string) => ({
rows: [{ id: 1, name: "Test User" }],
})),
end: mock(async () => {}),
};
}),
}));
test("opérations de base de données", async () => {
const { Database } = await import("./database");
const db = new Database();
const users = await db.getUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe("Test User");
});Fonctions mock globales
Effacer tous les mocks
Réinitialisez l'état de toutes les fonctions mock (appels, résultats, etc.) sans restaurer leur implémentation originale :
import { expect, mock, test } from "bun:test";
const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());
test("effacer tous les mocks", () => {
random1();
random2();
expect(random1).toHaveBeenCalledTimes(1);
expect(random2).toHaveBeenCalledTimes(1);
mock.clearAllMocks();
expect(random1).toHaveBeenCalledTimes(0);
expect(random2).toHaveBeenCalledTimes(0);
// Note : les implémentations sont préservées
expect(typeof random1()).toBe("number");
expect(typeof random2()).toBe("number");
});Cela réinitialise les propriétés .mock.calls, .mock.instances, .mock.contexts et .mock.results de tous les mocks, mais contrairement à mock.restore(), cela ne restaure pas l'implémentation originale.
Restaurer tous les mocks
Au lieu de restaurer manuellement chaque mock individuellement avec mockFn.mockRestore(), restaurez tous les mocks en une seule commande en appelant mock.restore(). Cela ne réinitialise pas la valeur des modules remplacés avec mock.module().
import { expect, mock, spyOn, test } from "bun:test";
import * as fooModule from "./foo.ts";
import * as barModule from "./bar.ts";
import * as bazModule from "./baz.ts";
test("foo, bar, baz", () => {
const fooSpy = spyOn(fooModule, "foo");
const barSpy = spyOn(barModule, "bar");
const bazSpy = spyOn(bazModule, "baz");
// Valeurs originales
expect(fooSpy).toBe("foo");
expect(barSpy).toBe("bar");
expect(bazSpy).toBe("baz");
// Implémentations mock
fooSpy.mockImplementation(() => 42);
barSpy.mockImplementation(() => 43);
bazSpy.mockImplementation(() => 44);
expect(fooSpy()).toBe(42);
expect(barSpy()).toBe(43);
expect(bazSpy()).toBe(44);
// Restaurer tout
mock.restore();
expect(fooSpy()).toBe("foo");
expect(barSpy()).toBe("bar");
expect(bazSpy()).toBe("baz");
});L'utilisation de mock.restore() peut réduire la quantité de code dans vos tests en l'ajoutant aux blocs afterEach dans chaque fichier de test ou même dans votre code de preload de test.
Compatibilité Vitest
Pour une compatibilité accrue avec les tests écrits pour Vitest, Bun fournit l'objet global vi comme alias pour certaines parties de l'API de mocking Jest :
import { test, expect } from "bun:test";
// Utiliser l'alias 'vi' similaire à Vitest
test("compatibilité vitest", () => {
const mockFn = vi.fn(() => 42);
mockFn();
expect(mockFn).toHaveBeenCalled();
// Les fonctions suivantes sont disponibles sur l'objet vi :
// vi.fn
// vi.spyOn
// vi.mock
// vi.restoreAllMocks
// vi.clearAllMocks
});Cela facilite le port des tests de Vitest vers Bun sans avoir à réécrire tous vos mocks.
Détails d'implémentation
Comprendre comment mock.module() fonctionne vous aide à l'utiliser plus efficacement :
Interaction avec le cache
Les mocks de module interagissent avec les caches de modules ESM et CommonJS.
Évaluation paresseuse
Le callback de la factory de mock est uniquement évalué lorsque le module est réellement importé ou requis.
Résolution de chemin
Bun résout automatiquement le spécificateur de module comme si vous faisiez un import, prenant en charge :
- Les chemins relatifs (
'./module') - Les chemins absolus (
'/path/to/module') - Les noms de package (
'lodash')
Effets de timing d'import
- Lors du mock avant le premier import : Aucun effet de bord du module original ne se produit
- Lors du mock après l'import : Les effets de bord du module original se sont déjà produits
Pour cette raison, l'utilisation de --preload est recommandée pour les mocks qui doivent empêcher les effets de bord.
Liaisons live
Les modules ESM mockés maintiennent des liaisons live, donc changer le mock mettra à jour tous les imports existants.
Motifs avancés
Fonctions factory
import { mock } from "bun:test";
function createMockUser(overrides = {}) {
return {
id: "mock-id",
name: "Mock User",
email: "mock@example.com",
...overrides,
};
}
const mockUserService = {
getUser: mock(async (id: string) => createMockUser({ id })),
createUser: mock(async (data: any) => createMockUser(data)),
updateUser: mock(async (id: string, data: any) => createMockUser({ id, ...data })),
};Mock conditionnel
import { test, expect, mock } from "bun:test";
const shouldUseMockApi = process.env.NODE_ENV === "test";
if (shouldUseMockApi) {
mock.module("./api", () => ({
fetchData: mock(async () => ({ data: "mocked" })),
}));
}
test("utilisation conditionnelle de l'API", async () => {
const { fetchData } = await import("./api");
const result = await fetchData();
if (shouldUseMockApi) {
expect(result.data).toBe("mocked");
}
});Motifs de nettoyage des mocks
import { afterEach, beforeEach } from "bun:test";
beforeEach(() => {
// Configurer des mocks communs
mock.module("./logger", () => ({
log: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
}));
});
afterEach(() => {
// Nettoyer tous les mocks
mock.restore();
mock.clearAllMocks();
});Bonnes pratiques
Garder les mocks simples
// Bien : Mock simple et ciblé
const mockUserApi = {
getUser: mock(async id => ({ id, name: "Test User" })),
};
// Éviter : Comportement mock trop complexe
const complexMock = mock(input => {
if (input.type === "A") {
return processTypeA(input);
} else if (input.type === "B") {
return processTypeB(input);
}
// ... beaucoup de logique complexe
});Utiliser des mocks typés
interface UserService {
getUser(id: string): Promise<User>;
createUser(data: CreateUserData): Promise<User>;
}
const mockUserService: UserService = {
getUser: mock(async (id: string) => ({ id, name: "Test User" })),
createUser: mock(async data => ({ id: "new-id", ...data })),
};Tester le comportement des mocks
test("le service appelle correctement l'API", async () => {
const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };
const service = new UserService(mockApi);
await service.getUser("123");
// Vérifier que le mock a été appelé correctement
expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});Notes
Auto-mocking
Le répertoire __mocks__ et l'auto-mocking ne sont pas encore pris en charge. Si cela vous empêche de passer à Bun, veuillez ouvrir un ticket.
ESM vs CommonJS
Les mocks de module ont des implémentations différentes pour les modules ESM et CommonJS. Pour les modules ES, Bun a ajouté des correctifs à JavaScriptCore qui permettent à Bun de remplacer les valeurs d'export à l'exécution et de mettre à jour les liaisons live de manière récursive.