El mocking es esencial para pruebas permitiendo reemplazar dependencias con implementaciones controladas. Bun proporciona capacidades completas de mocking incluyendo mocks de funciones, spies y mocks de módulos.
Mocks de funciones básicas
Crea mocks con la función 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);
});Compatibilidad con Jest
Alternativamente, puedes usar la función jest.fn(), como en Jest. Se comporta idénticamente.
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);
});Propiedades de funciones mock
El resultado de mock() es una nueva función que ha sido decorada con algunas propiedades adicionales.
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 }
// ]Propiedades y métodos disponibles
Las siguientes propiedades y métodos están implementados en funciones mock:
| Propiedad/Método | Descripción |
|---|---|
mockFn.getMockName() | Devuelve el nombre del mock |
mockFn.mock.calls | Array de argumentos de llamada para cada invocación |
mockFn.mock.results | Array de valores de retorno para cada invocación |
mockFn.mock.instances | Array de contextos this para cada invocación |
mockFn.mock.contexts | Array de contextos this para cada invocación |
mockFn.mock.lastCall | Argumentos de la llamada más reciente |
mockFn.mockClear() | Limpia el historial de llamadas |
mockFn.mockReset() | Limpia el historial de llamadas y elimina la implementación |
mockFn.mockRestore() | Restaura la implementación original |
mockFn.mockImplementation(fn) | Establece una nueva implementación |
mockFn.mockImplementationOnce(fn) | Establece implementación solo para la siguiente llamada |
mockFn.mockName(name) | Establece el nombre del mock |
mockFn.mockReturnThis() | Establece el valor de retorno a this |
mockFn.mockReturnValue(value) | Establece un valor de retorno |
mockFn.mockReturnValueOnce(value) | Establece valor de retorno solo para la siguiente llamada |
mockFn.mockResolvedValue(value) | Establece un valor de Promesa resuelta |
mockFn.mockResolvedValueOnce(value) | Establece Promesa resuelta solo para la siguiente llamada |
mockFn.mockRejectedValue(value) | Establece un valor de Promesa rechazada |
mockFn.mockRejectedValueOnce(value) | Establece Promesa rechazada solo para la siguiente llamada |
mockFn.withImplementation(fn, callback) | Cambia temporalmente la implementación |
Ejemplos prácticos
Uso básico de mock
import { test, expect, mock } from "bun:test";
test("comportamiento de función mock", () => {
const mockFn = mock((x: number) => x * 2);
// Llamar al mock
const result1 = mockFn(5);
const result2 = mockFn(10);
// Verificar llamadas
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith(5);
expect(mockFn).toHaveBeenLastCalledWith(10);
// Verificar resultados
expect(result1).toBe(10);
expect(result2).toBe(20);
// Inspeccionar historial de llamadas
expect(mockFn.mock.calls).toEqual([[5], [10]]);
expect(mockFn.mock.results).toEqual([
{ type: "return", value: 10 },
{ type: "return", value: 20 },
]);
});Implementaciones dinámicas de mock
import { test, expect, mock } from "bun:test";
test("implementaciones dinámicas de mock", () => {
const mockFn = mock();
// Establecer diferentes implementaciones
mockFn.mockImplementationOnce(() => "primero");
mockFn.mockImplementationOnce(() => "segundo");
mockFn.mockImplementation(() => "por defecto");
expect(mockFn()).toBe("primero");
expect(mockFn()).toBe("segundo");
expect(mockFn()).toBe("por defecto");
expect(mockFn()).toBe("por defecto"); // Usa implementación por defecto
});Mocks async
import { test, expect, mock } from "bun:test";
test("funciones mock async", async () => {
const asyncMock = mock();
// Mock de valores resueltos
asyncMock.mockResolvedValueOnce("primer resultado");
asyncMock.mockResolvedValue("resultado por defecto");
expect(await asyncMock()).toBe("primer resultado");
expect(await asyncMock()).toBe("resultado por defecto");
// Mock de valores rechazados
const rejectMock = mock();
rejectMock.mockRejectedValue(new Error("Error de mock"));
await expect(rejectMock()).rejects.toThrow("Error de mock");
});Spies con spyOn()
Es posible rastrear llamadas a una función sin reemplazarla con un mock. Usa spyOn() para crear un spy; estos spies pueden pasarse a .toHaveBeenCalled() y .toHaveBeenCalledTimes().
import { test, expect, spyOn } from "bun:test";
const ringo = {
name: "Ringo",
sayHi() {
console.log(`Hola soy ${this.name}`);
},
};
const spy = spyOn(ringo, "sayHi");
test("spyon", () => {
expect(spy).toHaveBeenCalledTimes(0);
ringo.sayHi();
expect(spy).toHaveBeenCalledTimes(1);
});Uso avanzado de spy
import { test, expect, spyOn, afterEach } from "bun:test";
class UserService {
async getUser(id: string) {
// Implementación original
return { id, name: `Usuario ${id}` };
}
async saveUser(user: any) {
// Implementación original
return { ...user, saved: true };
}
}
const userService = new UserService();
afterEach(() => {
// Restaurar todos los spies después de cada prueba
jest.restoreAllMocks();
});
test("spy en métodos de servicio", async () => {
// Spy sin cambiar implementación
const getUserSpy = spyOn(userService, "getUser");
const saveUserSpy = spyOn(userService, "saveUser");
// Usar el servicio normalmente
const user = await userService.getUser("123");
await userService.saveUser(user);
// Verificar llamadas
expect(getUserSpy).toHaveBeenCalledWith("123");
expect(saveUserSpy).toHaveBeenCalledWith(user);
});
test("spy con implementación mock", async () => {
// Spy y anular implementación
const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
id: "123",
name: "Usuario Mockeado",
});
const result = await userService.getUser("123");
expect(result.name).toBe("Usuario Mockeado");
expect(getUserSpy).toHaveBeenCalledWith("123");
});Mocks de módulos con mock.module()
El mocking de módulos te permite anular el comportamiento de un módulo. Usa mock.module(path: string, callback: () => Object) para mockear un módulo.
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");
});Como el resto de Bun, los mocks de módulos admiten tanto import como require.
Anular módulos ya importados
Si necesitas anular un módulo que ya ha sido importado, no hay nada especial que debas hacer. Solo llama a mock.module() y el módulo será anulado.
import { test, expect, mock } from "bun:test";
// El módulo que vamos a mockear está aquí:
import { foo } from "./module";
test("mock.module", async () => {
const cjs = require("./module");
expect(foo).toBe("bar");
expect(cjs.foo).toBe("bar");
// Lo actualizamos aquí:
mock.module("./module", () => {
return {
foo: "baz",
};
});
// Y los live bindings se actualizan.
expect(foo).toBe("baz");
// El módulo también se actualiza para CJS.
expect(cjs.foo).toBe("baz");
});Hoisting y precarga
Si necesitas asegurar que un módulo sea mockeado antes de ser importado, debes usar --preload para cargar tus mocks antes de que se ejecuten tus pruebas.
import { mock } from "bun:test";
mock.module("./module", () => {
return {
foo: "bar",
};
});bun test --preload ./my-preloadPara facilitarte la vida, puedes poner la precarga en tu bunfig.toml:
[test]
# Cargar estos módulos antes de ejecutar pruebas.
preload = ["./my-preload"]Mejores prácticas de mocks de módulos
Cuándo usar precarga
¿Qué pasa si mockeo un módulo que ya ha sido importado?
Si mockeas un módulo que ya ha sido importado, el módulo se actualizará en la caché de módulos. Esto significa que cualquier módulo que importe el módulo obtendrá la versión mockeada, PERO el módulo original todavía habrá sido evaluado. Eso significa que cualquier efecto secundario del módulo original todavía habrá ocurrido.
Si quieres prevenir que el módulo original sea evaluado, debes usar --preload para cargar tus mocks antes de que se ejecuten tus pruebas.
Ejemplos prácticos de mocks de módulos
import { test, expect, mock, beforeEach } from "bun:test";
// Mockear el módulo del cliente API
mock.module("./api-client", () => ({
fetchUser: mock(async (id: string) => ({ id, name: `Usuario ${id}` })),
createUser: mock(async (user: any) => ({ ...user, id: "new-id" })),
updateUser: mock(async (id: string, user: any) => ({ ...user, id })),
}));
test("servicio de usuario con API mockeada", 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("Usuario 123");
});Mockear dependencias externas
import { test, expect, mock } from "bun:test";
// Mockear biblioteca de base de datos externa
mock.module("pg", () => ({
Client: mock(function () {
return {
connect: mock(async () => {}),
query: mock(async (sql: string) => ({
rows: [{ id: 1, name: "Usuario de prueba" }],
})),
end: mock(async () => {}),
};
}),
}));
test("operaciones de base de datos", async () => {
const { Database } = await import("./database");
const db = new Database();
const users = await db.getUsers();
expect(users).toHaveLength(1);
expect(users[0].name).toBe("Usuario de prueba");
});Funciones mock globales
Limpiar todos los mocks
Restablece el estado de todas las funciones mock (llamadas, resultados, etc.) sin restaurar su implementación original:
import { expect, mock, test } from "bun:test";
const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());
test("limpiando todos los mocks", () => {
random1();
random2();
expect(random1).toHaveBeenCalledTimes(1);
expect(random2).toHaveBeenCalledTimes(1);
mock.clearAllMocks();
expect(random1).toHaveBeenCalledTimes(0);
expect(random2).toHaveBeenCalledTimes(0);
// Nota: las implementaciones se preservan
expect(typeof random1()).toBe("number");
expect(typeof random2()).toBe("number");
});Esto restablece las propiedades .mock.calls, .mock.instances, .mock.contexts y .mock.results de todos los mocks, pero a diferencia de mock.restore(), no restaura la implementación original.
Restaurar todos los mocks
En lugar de restaurar manualmente cada mock individualmente con mockFn.mockRestore(), restaura todos los mocks con un comando llamando a mock.restore(). Hacer esto no restablece el valor de los módulos anulados 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");
// Valores originales
expect(fooSpy).toBe("foo");
expect(barSpy).toBe("bar");
expect(bazSpy).toBe("baz");
// Implementaciones mockeadas
fooSpy.mockImplementation(() => 42);
barSpy.mockImplementation(() => 43);
bazSpy.mockImplementation(() => 44);
expect(fooSpy()).toBe(42);
expect(barSpy()).toBe(43);
expect(bazSpy()).toBe(44);
// Restaurar todos
mock.restore();
expect(fooSpy()).toBe("foo");
expect(barSpy()).toBe("bar");
expect(bazSpy()).toBe("baz");
});Usar mock.restore() puede reducir la cantidad de código en tus pruebas agregándolo a bloques afterEach en cada archivo de prueba o incluso en tu código de precarga de pruebas.
Compatibilidad con Vitest
Para mayor compatibilidad con pruebas escritas para Vitest, Bun proporciona el objeto global vi como un alias para partes de la API de mocking de Jest:
import { test, expect } from "bun:test";
// Usando el alias 'vi' similar a Vitest
test("compatibilidad con vitest", () => {
const mockFn = vi.fn(() => 42);
mockFn();
expect(mockFn).toHaveBeenCalled();
// Las siguientes funciones están disponibles en el objeto vi:
// vi.fn
// vi.spyOn
// vi.mock
// vi.restoreAllMocks
// vi.clearAllMocks
});Esto facilita portar pruebas de Vitest a Bun sin tener que reescribir todos tus mocks.
Detalles de implementación
Entender cómo funciona mock.module() te ayuda a usarlo más efectivamente:
Interacción con caché
Los mocks de módulos interactúan con las cachés de módulos ESM y CommonJS.
Evaluación perezosa
La devolución de llamada de la fábrica de mocks solo se evalúa cuando el módulo es realmente importado o requerido.
Resolución de rutas
Bun resuelve automáticamente el especificador del módulo como si estuvieras haciendo un import, soportando:
- Rutas relativas (
'./module') - Rutas absolutas (
'/path/to/module') - Nombres de paquetes (
'lodash')
Efectos de tiempo de importación
- Al mockear antes de la primera importación: No ocurren efectos secundarios del módulo original
- Al mockear después de la importación: Los efectos secundarios del módulo original ya han ocurrido
Por esta razón, se recomienda usar --preload para mocks que necesitan prevenir efectos secundarios.
Live Bindings
Los módulos ESM mockeados mantienen live bindings, así que cambiar el mock actualizará todas las importaciones existentes.
Patrones avanzados
Funciones de fábrica
import { mock } from "bun:test";
function createMockUser(overrides = {}) {
return {
id: "mock-id",
name: "Usuario Mock",
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 })),
};Mocking condicional
import { test, expect, mock } from "bun:test";
const shouldUseMockApi = process.env.NODE_ENV === "test";
if (shouldUseMockApi) {
mock.module("./api", () => ({
fetchData: mock(async () => ({ data: "mockeado" })),
}));
}
test("uso condicional de API", async () => {
const { fetchData } = await import("./api");
const result = await fetchData();
if (shouldUseMockApi) {
expect(result.data).toBe("mockeado");
}
});Patrones de limpieza de mocks
import { afterEach, beforeEach } from "bun:test";
beforeEach(() => {
// Configurar mocks comunes
mock.module("./logger", () => ({
log: mock(() => {}),
error: mock(() => {}),
warn: mock(() => {}),
}));
});
afterEach(() => {
// Limpiar todos los mocks
mock.restore();
mock.clearAllMocks();
});Mejores prácticas
Mantener mocks simples
// Bueno: Mock simple y enfocado
const mockUserApi = {
getUser: mock(async id => ({ id, name: "Usuario de prueba" })),
};
// Evitar: Comportamiento de mock overly complejo
const complexMock = mock(input => {
if (input.type === "A") {
return processTypeA(input);
} else if (input.type === "B") {
return processTypeB(input);
}
// ... mucha lógica compleja
});Usar mocks con tipos seguros
interface UserService {
getUser(id: string): Promise<User>;
createUser(data: CreateUserData): Promise<User>;
}
const mockUserService: UserService = {
getUser: mock(async (id: string) => ({ id, name: "Usuario de prueba" })),
createUser: mock(async data => ({ id: "new-id", ...data })),
};Probar comportamiento de mocks
test("servicio llama a API correctamente", async () => {
const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };
const service = new UserService(mockApi);
await service.getUser("123");
// Verificar que el mock fue llamado correctamente
expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});Notas
Auto-mocking
El directorio __mocks__ y el auto-mocking aún no están soportados. Si esto te impide cambiar a Bun, por favor abre un issue.
ESM vs CommonJS
Los mocks de módulos tienen diferentes implementaciones para módulos ESM y CommonJS. Para ES Modules, Bun ha agregado parches a JavaScriptCore que permiten a Bun anular valores de exportación en tiempo de ejecución y actualizar live bindings recursivamente.