Skip to content

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.

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);
});

Compatibilidad con Jest

Alternativamente, puedes usar la función jest.fn(), como en Jest. Se comporta idénticamente.

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);
});

Propiedades de funciones mock

El resultado de mock() es una nueva función que ha sido decorada con algunas propiedades adicionales.

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 }
//  ]

Propiedades y métodos disponibles

Las siguientes propiedades y métodos están implementados en funciones mock:

Propiedad/MétodoDescripción
mockFn.getMockName()Devuelve el nombre del mock
mockFn.mock.callsArray de argumentos de llamada para cada invocación
mockFn.mock.resultsArray de valores de retorno para cada invocación
mockFn.mock.instancesArray de contextos this para cada invocación
mockFn.mock.contextsArray de contextos this para cada invocación
mockFn.mock.lastCallArgumentos 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

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

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

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

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

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

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

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.

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

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

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

Para facilitarte la vida, puedes poner la precarga en tu bunfig.toml:

bunfig.toml
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

api-client.test.ts
ts
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

database.test.ts
ts
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:

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

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");

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

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

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

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

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

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

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

Bun por www.bunjs.com.cn editar