Skip to content

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.

ts
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.

ts
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.

ts
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éthodeDescription
mockFn.getMockName()Retourne le nom du mock
mockFn.mock.callsTableau des arguments d'appel pour chaque invocation
mockFn.mock.resultsTableau des valeurs de retour pour chaque invocation
mockFn.mock.instancesTableau des contextes this pour chaque invocation
mockFn.mock.contextsTableau des contextes this pour chaque invocation
mockFn.mock.lastCallArguments 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

ts
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

ts
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

ts
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().

ts
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

ts
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.

ts
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é.

ts
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.

ts
import { mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});
bash
bun test --preload ./my-preload

Pour vous faciliter la vie, vous pouvez mettre le preload dans votre bunfig.toml :

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

ts
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

ts
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 :

ts
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().

ts
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 :

ts
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

ts
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

ts
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

ts
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

ts
// 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

ts
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

ts
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.

Bun édité par www.bunjs.com.cn