Skip to content

Mocking ist für das Testen unerlässlich, da es Ihnen ermöglicht, Abhängigkeiten durch kontrollierte Implementierungen zu ersetzen. Bun bietet umfassende Mocking-Funktionen, einschließlich Funktions-Mocks, Spies und Modul-Mocks.

Einfache Funktions-Mocks

Erstellen Sie Mocks mit der mock-Funktion.

test.ts
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);
});

Jest-Kompatibilität

Alternativ können Sie die jest.fn()-Funktion wie in Jest verwenden. Sie verhält sich identisch.

test.ts
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);
});

Mock-Funktionseigenschaften

Das Ergebnis von mock() ist eine neue Funktion, die mit einigen zusätzlichen Eigenschaften dekoriert wurde.

test.ts
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 }
//  ]

Verfügbare Eigenschaften und Methoden

Die folgenden Eigenschaften und Methoden sind auf Mock-Funktionen implementiert:

Eigenschaft/MethodeBeschreibung
mockFn.getMockName()Gibt den Mock-Namen zurück
mockFn.mock.callsArray mit Aufrufargumenten für jeden Aufruf
mockFn.mock.resultsArray mit Rückgabewerten für jeden Aufruf
mockFn.mock.instancesArray mit this-Kontexten für jeden Aufruf
mockFn.mock.contextsArray mit this-Kontexten für jeden Aufruf
mockFn.mock.lastCallArgumente des letzten Aufrufs
mockFn.mockClear()Löscht die Aufrufhistorie
mockFn.mockReset()Löscht die Aufrufhistorie und entfernt die Implementierung
mockFn.mockRestore()Stellt die ursprüngliche Implementierung wieder her
mockFn.mockImplementation(fn)Setzt eine neue Implementierung
mockFn.mockImplementationOnce(fn)Setzt die Implementierung nur für den nächsten Aufruf
mockFn.mockName(name)Setzt den Mock-Namen
mockFn.mockReturnThis()Setzt den Rückgabewert auf this
mockFn.mockReturnValue(value)Setzt einen Rückgabewert
mockFn.mockReturnValueOnce(value)Setzt den Rückgabewert nur für den nächsten Aufruf
mockFn.mockResolvedValue(value)Setzt einen aufgelösten Promise-Wert
mockFn.mockResolvedValueOnce(value)Setzt aufgelösten Promise nur für den nächsten Aufruf
mockFn.mockRejectedValue(value)Setzt einen abgelehnten Promise-Wert
mockFn.mockRejectedValueOnce(value)Setzt abgelehnten Promise nur für den nächsten Aufruf
mockFn.withImplementation(fn, callback)Ändert vorübergehend die Implementierung

Praktische Beispiele

Einfache Mock-Verwendung

test.ts
ts
import { test, expect, mock } from "bun:test";

test("Mock-Funktionsverhalten", () => {
  const mockFn = mock((x: number) => x * 2);

  // Mock aufrufen
  const result1 = mockFn(5);
  const result2 = mockFn(10);

  // Aufrufe überprüfen
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith(5);
  expect(mockFn).toHaveBeenLastCalledWith(10);

  // Ergebnisse überprüfen
  expect(result1).toBe(10);
  expect(result2).toBe(20);

  // Aufrufhistorie inspizieren
  expect(mockFn.mock.calls).toEqual([[5], [10]]);
  expect(mockFn.mock.results).toEqual([
    { type: "return", value: 10 },
    { type: "return", value: 20 },
  ]);
});

Dynamische Mock-Implementierungen

test.ts
ts
import { test, expect, mock } from "bun:test";

test("dynamische Mock-Implementierungen", () => {
  const mockFn = mock();

  // Unterschiedliche Implementierungen setzen
  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"); // Verwendet Standard-Implementierung
});

Asynchrone Mocks

test.ts
ts
import { test, expect, mock } from "bun:test";

test("asynchrone Mock-Funktionen", async () => {
  const asyncMock = mock();

  // Aufgelöste Werte mocken
  asyncMock.mockResolvedValueOnce("erstes Ergebnis");
  asyncMock.mockResolvedValue("Standardergebnis");

  expect(await asyncMock()).toBe("erstes Ergebnis");
  expect(await asyncMock()).toBe("Standardergebnis");

  // Abgelehnte Werte mocken
  const rejectMock = mock();
  rejectMock.mockRejectedValue(new Error("Mock-Fehler"));

  await expect(rejectMock()).rejects.toThrow("Mock-Fehler");
});

Spies mit spyOn()

Es ist möglich, Aufrufe einer Funktion zu verfolgen, ohne sie durch einen Mock zu ersetzen. Verwenden Sie spyOn(), um einen Spy zu erstellen; diese Spies können an .toHaveBeenCalled() und .toHaveBeenCalledTimes() übergeben werden.

test.ts
ts
import { test, expect, spyOn } from "bun:test";

const ringo = {
  name: "Ringo",
  sayHi() {
    console.log(`Hallo, ich bin ${this.name}`);
  },
};

const spy = spyOn(ringo, "sayHi");

test("spyOn", () => {
  expect(spy).toHaveBeenCalledTimes(0);
  ringo.sayHi();
  expect(spy).toHaveBeenCalledTimes(1);
});

Fortgeschrittene Spy-Verwendung

test.ts
ts
import { test, expect, spyOn, afterEach } from "bun:test";

class UserService {
  async getUser(id: string) {
    // Ursprüngliche Implementierung
    return { id, name: `Benutzer ${id}` };
  }

  async saveUser(user: any) {
    // Ursprüngliche Implementierung
    return { ...user, saved: true };
  }
}

const userService = new UserService();

afterEach(() => {
  // Alle Spies nach jedem Test wiederherstellen
  jest.restoreAllMocks();
});

test("Service-Methoden ausspähen", async () => {
  // Spy ohne Änderung der Implementierung
  const getUserSpy = spyOn(userService, "getUser");
  const saveUserSpy = spyOn(userService, "saveUser");

  // Service normal verwenden
  const user = await userService.getUser("123");
  await userService.saveUser(user);

  // Aufrufe überprüfen
  expect(getUserSpy).toHaveBeenCalledWith("123");
  expect(saveUserSpy).toHaveBeenCalledWith(user);
});

test("Spy mit Mock-Implementierung", async () => {
  // Spy und Implementierung überschreiben
  const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
    id: "123",
    name: "Gemockter Benutzer",
  });

  const result = await userService.getUser("123");

  expect(result.name).toBe("Gemockter Benutzer");
  expect(getUserSpy).toHaveBeenCalledWith("123");
});

Modul-Mocks mit mock.module()

Modul-Mocking ermöglicht es Ihnen, das Verhalten eines Moduls zu überschreiben. Verwenden Sie mock.module(path: string, callback: () => Object), um ein Modul zu mocken.

test.ts
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");
});

Wie der Rest von Bun unterstützt Modul-Mocking sowohl import als auch require.

Bereits importierte Module überschreiben

Wenn Sie ein Modul überschreiben müssen, das bereits importiert wurde, müssen Sie nichts Besonderes tun. Rufen Sie einfach mock.module() auf und das Modul wird überschrieben.

test.ts
ts
import { test, expect, mock } from "bun:test";

// Das Modul, das wir mocken werden, ist hier:
import { foo } from "./module";

test("mock.module", async () => {
  const cjs = require("./module");
  expect(foo).toBe("bar");
  expect(cjs.foo).toBe("bar");

  // Wir aktualisieren es hier:
  mock.module("./module", () => {
    return {
      foo: "baz",
    };
  });

  // Und die Live-Bindings werden aktualisiert.
  expect(foo).toBe("baz");

  // Das Modul wurde auch für CJS aktualisiert.
  expect(cjs.foo).toBe("baz");
});

Hoisting und Preloading

Wenn Sie sicherstellen müssen, dass ein Modul gemockt wird, bevor es importiert wird, sollten Sie --preload verwenden, um Ihre Mocks vor dem Ausführen Ihrer Tests zu laden.

my-preload.ts
ts
import { mock } from "bun:test";

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

Um es Ihnen einfacher zu machen, können Sie Preload in Ihre bunfig.toml einfügen:

bunfig.toml
toml
[test]
# Diese Module vor dem Ausführen von Tests laden.
preload = ["./my-preload"]

Modul-Mock-Best-Practices

Wann Preload verwenden

Was passiert, wenn ich ein Modul mocke, das bereits importiert wurde?

Wenn Sie ein Modul mocken, das bereits importiert wurde, wird das Modul im Modul-Cache aktualisiert. Das bedeutet, dass alle Module, die das Modul importieren, die gemockte Version erhalten, ABER das ursprüngliche Modul wurde dennoch ausgewertet. Das bedeutet, dass alle Seiteneffekte des ursprünglichen Moduls weiterhin stattgefunden haben.

Wenn Sie verhindern möchten, dass das ursprüngliche Modul ausgewertet wird, sollten Sie --preload verwenden, um Ihre Mocks vor dem Ausführen Ihrer Tests zu laden.

Praktische Modul-Mock-Beispiele

api-client.test.ts
ts
import { test, expect, mock, beforeEach } from "bun:test";

// Das API-Client-Modul mocken
mock.module("./api-client", () => ({
  fetchUser: mock(async (id: string) => ({ id, name: `Benutzer ${id}` })),
  createUser: mock(async (user: any) => ({ ...user, id: "neue-id" })),
  updateUser: mock(async (id: string, user: any) => ({ ...user, id })),
}));

test("Benutzer-Service mit gemocktem API", 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("Benutzer 123");
});

Externe Abhängigkeiten mocken

database.test.ts
ts
import { test, expect, mock } from "bun:test";

// Externe Datenbank-Bibliothek mocken
mock.module("pg", () => ({
  Client: mock(function () {
    return {
      connect: mock(async () => {}),
      query: mock(async (sql: string) => ({
        rows: [{ id: 1, name: "Test Benutzer" }],
      })),
      end: mock(async () => {}),
    };
  }),
}));

test("Datenbank-Operationen", 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 Benutzer");
});

Globale Mock-Funktionen

Alle Mocks löschen

Setzen Sie den Zustand aller Mock-Funktionen (Aufrufe, Ergebnisse usw.) zurück, ohne ihre ursprüngliche Implementierung wiederherzustellen:

test.ts
ts
import { expect, mock, test } from "bun:test";

const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());

test("Alle Mocks löschen", () => {
  random1();
  random2();

  expect(random1).toHaveBeenCalledTimes(1);
  expect(random2).toHaveBeenCalledTimes(1);

  mock.clearAllMocks();

  expect(random1).toHaveBeenCalledTimes(0);
  expect(random2).toHaveBeenCalledTimes(0);

  // Hinweis: Implementierungen bleiben erhalten
  expect(typeof random1()).toBe("number");
  expect(typeof random2()).toBe("number");
});

Dies setzt die .mock.calls-, .mock.instances-, .mock.contexts- und .mock.results-Eigenschaften aller Mocks zurück, aber im Gegensatz zu mock.restore() stellt es nicht die ursprüngliche Implementierung wieder her.

Alle Mocks wiederherstellen

Anstatt jeden Mock einzeln mit mockFn.mockRestore() manuell wiederherzustellen, stellen Sie alle Mocks mit einem Befehl wieder her, indem Sie mock.restore() aufrufen. Dies setzt den Wert von mit mock.module() überschriebenen Modulen nicht zurück.

test.ts
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");

  // Ursprüngliche Werte
  expect(fooSpy).toBe("foo");
  expect(barSpy).toBe("bar");
  expect(bazSpy).toBe("baz");

  // Mock-Implementierungen
  fooSpy.mockImplementation(() => 42);
  barSpy.mockImplementation(() => 43);
  bazSpy.mockImplementation(() => 44);

  expect(fooSpy()).toBe(42);
  expect(barSpy()).toBe(43);
  expect(bazSpy()).toBe(44);

  // Alle wiederherstellen
  mock.restore();

  expect(fooSpy()).toBe("foo");
  expect(barSpy()).toBe("bar");
  expect(bazSpy()).toBe("baz");
});

Die Verwendung von mock.restore() kann die Menge an Code in Ihren Tests reduzieren, indem Sie es zu afterEach-Blöcken in jeder Testdatei oder sogar in Ihrem Test-Preload-Code hinzufügen.

Vitest-Kompatibilität

Für zusätzliche Kompatibilität mit für Vitest geschriebenen Tests stellt Bun das vi-globale Objekt als Alias für Teile der Jest-Mocking-API bereit:

test.ts
ts
import { test, expect } from "bun:test";

// Verwendung des 'vi'-Alias ähnlich wie Vitest
test("Vitest-Kompatibilität", () => {
  const mockFn = vi.fn(() => 42);

  mockFn();
  expect(mockFn).toHaveBeenCalled();

  // Die folgenden Funktionen sind auf dem vi-Objekt verfügbar:
  // vi.fn
  // vi.spyOn
  // vi.mock
  // vi.restoreAllMocks
  // vi.clearAllMocks
});

Dies erleichtert das Portieren von Tests von Vitest nach Bun, ohne alle Ihre Mocks umschreiben zu müssen.

Implementierungsdetails

Das Verständnis der Funktionsweise von mock.module() hilft Ihnen, es effektiver zu verwenden:

Cache-Interaktion

Modul-Mocks interagieren sowohl mit ESM- als auch mit CommonJS-Modul-Caches.

Lazy Evaluation

Der Mock-Factory-Callback wird nur ausgewertet, wenn das Modul tatsächlich importiert oder angefordert wird.

Pfadauflösung

Bun löst den Modulspezifizierer automatisch so auf, als würden Sie einen Import durchführen, und unterstützt:

  • Relative Pfade ('./module')
  • Absolute Pfade ('/path/to/module')
  • Paketnamen ('lodash')

Import-Timing-Effekte

  • Beim Mocken vor dem ersten Import: Es treten keine Seiteneffekte aus dem ursprünglichen Modul auf
  • Beim Mocken nach dem Import: Die Seiteneffekte des ursprünglichen Moduls sind bereits geschehen

Aus diesem Grund wird die Verwendung von --preload für Mocks empfohlen, die Seiteneffekte verhindern müssen.

Live-Bindings

Gemockte ESM-Module behalten Live-Bindings bei, sodass das Ändern des Mocks alle vorhandenen Importe aktualisiert.

Fortgeschrittene Muster

Factory-Funktionen

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

function createMockUser(overrides = {}) {
  return {
    id: "mock-id",
    name: "Mock Benutzer",
    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 })),
};

Bedingtes Mocking

test.ts
ts
import { test, expect, mock } from "bun:test";

const shouldUseMockApi = process.env.NODE_ENV === "test";

if (shouldUseMockApi) {
  mock.module("./api", () => ({
    fetchData: mock(async () => ({ data: "gemockt" })),
  }));
}

test("bedingte API-Verwendung", async () => {
  const { fetchData } = await import("./api");
  const result = await fetchData();

  if (shouldUseMockApi) {
    expect(result.data).toBe("gemockt");
  }
});

Mock-Aufräumungsmuster

test.ts
ts
import { afterEach, beforeEach } from "bun:test";

beforeEach(() => {
  // Häufige Mocks einrichten
  mock.module("./logger", () => ({
    log: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
  }));
});

afterEach(() => {
  // Alle Mocks aufräumen
  mock.restore();
  mock.clearAllMocks();
});

Best Practices

Mocks einfach halten

test.ts
ts
// Gut: Einfacher, fokussierter Mock
const mockUserApi = {
  getUser: mock(async id => ({ id, name: "Test Benutzer" })),
};

// Vermeiden: Übermäßig komplexes Mock-Verhalten
const complexMock = mock(input => {
  if (input.type === "A") {
    return processTypeA(input);
  } else if (input.type === "B") {
    return processTypeB(input);
  }
  // ... viel komplexe Logik
});

Typsichere Mocks verwenden

ts
interface UserService {
  getUser(id: string): Promise<User>;
  createUser(data: CreateUserData): Promise<User>;
}

const mockUserService: UserService = {
  getUser: mock(async (id: string) => ({ id, name: "Test Benutzer" })),
  createUser: mock(async data => ({ id: "neue-id", ...data })),
};

Mock-Verhalten testen

test.ts
ts
test("Service ruft API korrekt auf", async () => {
  const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };

  const service = new UserService(mockApi);
  await service.getUser("123");

  // Überprüfen, dass der Mock korrekt aufgerufen wurde
  expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
  expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});

Hinweise

Auto-Mocking

Das __mocks__-Verzeichnis und Auto-Mocking werden noch nicht unterstützt. Wenn dies Sie vom Wechsel zu Bun abhält, bitte ein Issue eröffnen.

ESM vs. CommonJS

Modul-Mocks haben unterschiedliche Implementierungen für ESM- und CommonJS-Module. Für ES-Module hat Bun Patches zu JavaScriptCore hinzugefügt, die es Bun ermöglichen, Exportwerte zur Laufzeit zu überschreiben und Live-Bindings rekursiv zu aktualisieren.

Bun von www.bunjs.com.cn bearbeitet