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.
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.
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.
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/Methode | Beschreibung |
|---|---|
mockFn.getMockName() | Gibt den Mock-Namen zurück |
mockFn.mock.calls | Array mit Aufrufargumenten für jeden Aufruf |
mockFn.mock.results | Array mit Rückgabewerten für jeden Aufruf |
mockFn.mock.instances | Array mit this-Kontexten für jeden Aufruf |
mockFn.mock.contexts | Array mit this-Kontexten für jeden Aufruf |
mockFn.mock.lastCall | Argumente 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
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
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
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.
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
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.
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.
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.
import { mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});bun test --preload ./my-preloadUm es Ihnen einfacher zu machen, können Sie Preload in Ihre bunfig.toml einfügen:
[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
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
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:
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.
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:
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
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
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
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
// 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
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("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.