Il mocking e essenziale per i test permettendoti di sostituire le dipendenze con implementazioni controllate. Bun fornisce capacita di mocking complete incluse funzioni mock, spy e mock di modulo.
Funzioni Mock Base
Crea mock con la funzione 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);
});Compatibilita con Jest
In alternativa, puoi usare la funzione jest.fn(), come in Jest. Si comporta in modo identico.
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);
});Proprieta delle Funzioni Mock
Il risultato di mock() e una nuova funzione che e stata decorata con alcune proprieta aggiuntive.
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 }
// ]Proprieta e Metodi Disponibili
Le seguenti proprieta e metodi sono implementati sulle funzioni mock:
| Proprieta/Metodo | Descrizione |
|---|---|
mockFn.getMockName() | Restituisce il nome del mock |
mockFn.mock.calls | Array degli argomenti di chiamata per ogni invocazione |
mockFn.mock.results | Array dei valori di ritorno per ogni invocazione |
mockFn.mock.instances | Array dei contesti this per ogni invocazione |
mockFn.mock.contexts | Array dei contesti this per ogni invocazione |
mockFn.mock.lastCall | Argomenti della chiamata piu recente |
mockFn.mockClear() | Cancella la cronologia delle chiamate |
mockFn.mockReset() | Cancella la cronologia delle chiamate e rimuove l'implementazione |
mockFn.mockRestore() | Ripristina l'implementazione originale |
mockFn.mockImplementation(fn) | Imposta una nuova implementazione |
mockFn.mockImplementationOnce(fn) | Imposta l'implementazione solo per la prossima chiamata |
mockFn.mockName(name) | Imposta il nome del mock |
mockFn.mockReturnThis() | Imposta il valore di ritorno a this |
mockFn.mockReturnValue(value) | Imposta un valore di ritorno |
mockFn.mockReturnValueOnce(value) | Imposta il valore di ritorno solo per la prossima chiamata |
mockFn.mockResolvedValue(value) | Imposta un valore Promise risolto |
mockFn.mockResolvedValueOnce(value) | Imposta il valore Promise risolto solo per la prossima chiamata |
mockFn.mockRejectedValue(value) | Imposta un valore Promise rifiutato |
mockFn.mockRejectedValueOnce(value) | Imposta il valore Promise rifiutato solo per la prossima chiamata |
mockFn.withImplementation(fn, callback) | Cambia temporaneamente l'implementazione |
Esempi Pratici
Uso Base del Mock
import { test, expect, mock } from "bun:test";
test("comportamento della funzione mock", () => {
const mockFn = mock((x: number) => x * 2);
// Chiama il mock
const result1 = mockFn(5);
const result2 = mockFn(10);
// Verifica le chiamate
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn).toHaveBeenLastCalledWith(10);
// Controlla i risultati
expect(result1).toBe(10);
expect(result2).toBe(20);
// Ispeziona la cronologia delle chiamate
expect(mockFn.mock.calls).toEqual([[5], [10]]);
expect(mockFn.mock.results).toEqual([
{ type: "return", value: 10 },
{ type: "return", value: 20 },
]);
});Implementazioni Mock Dinamiche
import { test, expect, mock } from "bun:test";
test("implementazioni mock dinamiche", () => {
const mockFn = mock();
// Imposta implementazioni diverse
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"); // Usa l'implementazione predefinita
});Mock Asincroni
import { test, expect, mock } from "bun:test";
test("funzioni mock asincrone", async () => {
const asyncMock = mock();
// Mock dei valori risolti
asyncMock.mockResolvedValueOnce("first result");
asyncMock.mockResolvedValue("default result");
expect(await asyncMock()).toBe("first result");
expect(await asyncMock()).toBe("default result");
// Mock dei valori rifiutati
const rejectMock = mock();
rejectMock.mockRejectedValue(new Error("Mock error"));
await expect(rejectMock()).rejects.toThrow("Mock error");
});Spy con spyOn()
E possibile tracciare le chiamate a una funzione senza sostituirla con un mock. Usa spyOn() per creare uno spy; questi spy possono essere passati a .toHaveBeenCalled() e .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);
});Uso Avanzato degli Spy
import { test, expect, spyOn, afterEach } from "bun:test";
class UserService {
async getUser(id: string) {
// Implementazione originale
return { id, name: `User ${id}` };
}
async saveUser(user: any) {
// Implementazione originale
return { ...user, saved: true };
}
}
const userService = new UserService();
afterEach(() => {
// Ripristina tutti gli spy dopo ogni test
jest.restoreAllMocks();
});
test("spy sui metodi del servizio", async () => {
// Spy senza cambiare l'implementazione
const getUserSpy = spyOn(userService, "getUser");
const saveUserSpy = spyOn(userService, "saveUser");
// Usa il servizio normalmente
const user = await userService.getUser("123");
await userService.saveUser(user);
// Verifica le chiamate
expect(getUserSpy).toHaveBeenCalledWith("123");
expect(saveUserSpy).toHaveBeenCalledWith(user);
});
test("spy con implementazione mock", async () => {
// Spy e override dell'implementazione
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");
});Mock di Modulo con mock.module()
Il mocking dei moduli ti permette di sovrascrivere il comportamento di un modulo. Usa mock.module(path: string, callback: () => Object) per mockare un modulo.
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");
});Come il resto di Bun, i mock di modulo supportano sia import che require.
Sovrascrivere Moduli Gia Importati
Se hai bisogno di sovrascrivere un modulo che e gia stato importato, non c'e nulla di speciale che devi fare. Chiama semplicemente mock.module() e il modulo sara sovrascritto.
import { test, expect, mock } from "bun:test";
// Il modulo che andremo a mockare e qui:
import { foo } from "./module";
test("mock.module", async () => {
const cjs = require("./module");
expect(foo).toBe("bar");
expect(cjs.foo).toBe("bar");
// Lo aggiorniamo qui:
mock.module("./module", () => {
return {
foo: "baz",
};
});
// E i binding live vengono aggiornati.
expect(foo).toBe("baz");
// Il modulo e aggiornato anche per CJS.
require.cjs.foo = "baz";
});Hoisting e Preloading
Se hai bisogno di assicurarti che un modulo sia mockato prima di essere importato, dovresti usare --preload per caricare i tuoi mock prima che i test vengano eseguiti.
import { mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});bun test --preload ./my-preloadPer renderti la vita piu facile, puoi mettere il preload nel tuo bunfig.toml:
[test]
# Carica questi moduli prima di eseguire i test.
preload = ["./my-preload"]Best Practice per i Mock di Modulo
Quando Usare il Preload
Cosa succede se mocko un modulo che e gia stato importato?
Se mocki un modulo che e gia stato importato, il modulo sara aggiornato nella cache dei moduli. Questo significa che qualsiasi modulo che importa il modulo ricevera la versione mockata, MA il modulo originale sara stato comunque valutato. Questo significa che qualsiasi effetto collaterale dal modulo originale si sara comunque verificato.
Se vuoi impedire che il modulo originale venga valutato, dovresti usare --preload per caricare i tuoi mock prima che i test vengano eseguiti.
Esempi Pratici di Mock di Modulo
import { test, expect, mock, beforeEach } from "bun:test";
// Mock del modulo del 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("servizio utente con API mockata", 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");
});Mock di Dipendenze Esterne
import { test, expect, mock } from "bun:test";
// Mock della libreria database esterna
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("operazioni database", 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");
});Funzioni Mock Globali
Cancella Tutti i Mock
Resetta lo stato di tutte le funzioni mock (chiamate, risultati, ecc.) senza ripristinare la loro implementazione originale:
import { expect, mock, test } from "bun:test";
const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());
test("cancellare tutti i mock", () => {
random1();
random2();
expect(random1).toHaveBeenCalledTimes(1);
expect(random2).toHaveBeenCalledTimes(1);
mock.clearAllMocks();
expect(random1).toHaveBeenCalledTimes(0);
expect(random2).toHaveBeenCalledTimes(0);
// Nota: le implementazioni sono preservate
expect(typeof random1()).toBe("number");
expect(typeof random2()).toBe("number");
});Questo resetta le proprieta .mock.calls, .mock.instances, .mock.contexts e .mock.results di tutti i mock, ma a differenza di mock.restore(), non ripristina l'implementazione originale.
Ripristina Tutti i Mock
Invece di ripristinare manualmente ogni mock singolarmente con mockFn.mockRestore(), ripristina tutti i mock con un comando chiamando mock.restore(). Fare questo non resetta il valore dei moduli sovrascritti con 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");
// Valori originali
expect(fooSpy).toBe("foo");
expect(barSpy).toBe("bar");
expect(bazSpy).toBe("baz");
// Implementazioni mock
fooSpy.mockImplementation(() => 42);
barSpy.mockImplementation(() => 43);
bazSpy.mockImplementation(() => 44);
expect(fooSpy()).toBe(42);
expect(barSpy()).toBe(43);
expect(bazSpy()).toBe(44);
// Ripristina tutto
mock.restore();
expect(fooSpy()).toBe("foo");
expect(barSpy()).toBe("bar");
expect(bazSpy()).toBe("baz");
});L'uso di mock.restore() puo ridurre la quantita di codice nei tuoi test aggiungendolo ai blocchi afterEach in ogni file di test o anche nel tuo codice di preload dei test.
Compatibilita con Vitest
Per una maggiore compatibilita con i test scritti per Vitest, Bun fornisce l'oggetto globale vi come alias per parti dell'API di mocking di Jest:
import { test, expect } from "bun:test";
// Usare l'alias 'vi' simile a Vitest
test("compatibilita vitest", () => {
const mockFn = vi.fn(() => 42);
mockFn();
expect(mockFn).toHaveBeenCalled();
// Le seguenti funzioni sono disponibili sull'oggetto vi:
// vi.fn
// vi.spyOn
// vi.mock
// vi.restoreAllMocks
// vi.clearAllMocks
});Questo rende piu facile portare i test da Vitest a Bun senza dover riscrivere tutti i tuoi mock.
Dettagli di Implementazione
Capire come funziona mock.module() ti aiuta a usarlo piu efficacemente:
Interazione con la Cache
I mock di modulo interagiscono sia con le cache dei moduli ESM che CommonJS.
Valutazione Lazy
Il callback della factory del mock viene valutato solo quando il modulo viene effettivamente importato o richiesto.
Risoluzione dei Percorsi
Bun risolve automaticamente il specificatore del modulo come se stessi facendo un import, supportando:
- Percorsi relativi (
'./module') - Percorsi assoluti (
'/path/to/module') - Nomi di pacchetti (
'lodash')
Effetti del Timing dell'Import
- Quando si mocka prima del primo import: Non si verificano effetti collaterali dal modulo originale
- Quando si mocka dopo l'import: Gli effetti collaterali del modulo originale si sono gia verificati
Per questo motivo, l'uso di --preload e raccomandato per i mock che devono prevenire effetti collaterali.
Binding Live
I moduli ESM mockati mantengono i binding live, quindi cambiare il mock aggiornerà tutti gli import esistenti.
Pattern Avanzati
Funzioni 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 Condizionale
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("uso condizionale dell'API", async () => {
const { fetchData } = await import("./api");
const result = await fetchData();
if (shouldUseMockApi) {
expect(result.data).toBe("mocked");
}
});Pattern di Pulizia dei Mock
import { afterEach, beforeEach } from "bun:test";
beforeEach(() => {
// Imposta i mock comuni
mock.module("./logger", () => ({
log: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
}));
});
afterEach(() => {
// Pulizia di tutti i mock
mock.restore();
mock.clearAllMocks();
});Best Practices
Mantieni i Mock Semplci
// Buono: Mock semplice e focalizzato
const mockUserApi = {
getUser: mock(async id => ({ id, name: "Test User" })),
};
// Evita: Comportamento del mock troppo complesso
const complexMock = mock(input => {
if (input.type === "A") {
return processTypeA(input);
} else if (input.type === "B") {
return processTypeB(input);
}
// ... molta logica complessa
});Usa Mock Type-Safe
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 })),
};Testa il Comportamento del Mock
test("il servizio chiama l'API correttamente", async () => {
const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };
const service = new UserService(mockApi);
await service.getUser("123");
// Verifica che il mock sia stato chiamato correttamente
expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});Note
Auto-mocking
La directory __mocks__ e l'auto-mocking non sono ancora supportati. Se questo ti impedisce di passare a Bun, per favore segnala un issue.
ESM vs CommonJS
I mock di modulo hanno implementazioni diverse per i moduli ESM e CommonJS. Per i Moduli ES, Bun ha aggiunto patch a JavaScriptCore che permettono a Bun di sovrascrivere i valori degli export a runtime e aggiornare ricorsivamente i binding live.