Saltar al contenido principal

bun:test — Testing con herramientas nativas de Bun

Cuándo usar bun:test — alcance y restricciones

Política Wiedii: bun:test se usa únicamente en herramientas CLI puras — proyectos que no requieren conexión a bases de datos, flujos HTTP/web, ni dependencias de runtime externas. Para cualquier otro tipo de proyecto (APIs, web apps, workers, SDKs con IO complejo) se debe evaluar Jest o Vitest según el stack.

Proyectos donde aplica bun:test

  • CLIs compiladas (Go, Bun, Node) sin IO externo en sus tests
  • Scripts de automatización y utilidades de línea de comandos
  • Librerías de lógica pura (parsers, transformadores, utilidades de fecha/string)
  • Paquetes de configuración compartida

Proyectos donde se debe evaluar Jest o Vitest

Tipo de proyectoFramework recomendadoRazón
API / backend con DBVitestMejor integración con testcontainers, soporte nativo para workers
Web app (Astro, Next.js, React)VitestIntegración con jsdom/happy-dom, ecosystem de plugins
SDK con múltiples targetsVitestModes node/browser/edge, configuración flexible por entorno
Proyecto con tests E2E complejosVitest + PlaywrightIntegración oficial, mejor reporte de flakiness

La elección entre Jest y Vitest se documenta en la nota correspondiente del stack. Si no hay nota previa, Vitest es la opción por defecto para proyectos nuevos que no sean CLI puras.

Por qué bun:test es suficiente para CLI puras

Bun incluye un test runner completo en bun:test que cubre el 100 % de los casos de herramientas CLI sin instalar Jest, Vitest ni ninguna dependencia adicional. La suite es compatible con la API de Jest/Vitest (describe, test, expect, mock, spyOn), corre TypeScript directamente, y es significativamente más rápida que cualquier alternativa basada en Node.

En herramientas CLI de Wiedii, la pirámide de tests tiene cuatro niveles: unit → integration → property → e2e. Todos usan bun test nativo.


Estructura de tests en proyectos Wiedii

packages/<pkg>/
src/
lib/
foo.ts
foo.spec.ts # unit
foo.integration.spec.ts # integration (IO real, tmp dirs)
foo.property.spec.ts # property (fast-check)
e2e/
foo.e2e.spec.ts # e2e (dist/ built bundle)

Scripts estándar en package.json de cada paquete:

{
"scripts": {
"test:unit": "find src -name '*.spec.ts' ! -name '*.integration.spec.ts' ! -name '*.property.spec.ts' -exec bun test {} +",
"test:integration": "find src -name '*.integration.spec.ts' -exec bun test {} +",
"test:property": "find src -name '*.property.spec.ts' -exec bun test {} +",
"test:e2e": "find e2e -name '*.e2e.spec.ts' -exec bun test {} +"
}
}

API básica — describe / test / expect

import { describe, test, expect, beforeEach, afterEach } from 'bun:test';

describe('calculator', () => {
test('adds two numbers', () => {
expect(2 + 2).toBe(4);
});

test('throws on division by zero', () => {
expect(() => divide(1, 0)).toThrow('division by zero');
});

test('async operation resolves', async () => {
await expect(fetchUser(1)).resolves.toMatchObject({ id: 1 });
});
});

Mock de funciones — mock() / spyOn()

Función stub

import { mock, test, expect } from 'bun:test';

const greet = mock((name: string) => `Hello, ${name}!`);

greet('Ana');
expect(greet).toHaveBeenCalledWith('Ana');
expect(greet).toHaveBeenCalledTimes(1);
expect(greet.mock.calls[0]).toEqual(['Ana']);

Spy sobre objeto existente

import { spyOn, test, expect, afterEach } from 'bun:test';
import * as fs from 'node:fs/promises';

afterEach(() => {
// Restaurar todos los spies después de cada test
mock.restore();
});

test('calls readFile with correct path', async () => {
const spy = spyOn(fs, 'readFile').mockResolvedValue(Buffer.from('content'));
await readConfig('/etc/config.json');
expect(spy).toHaveBeenCalledWith('/etc/config.json');
});

Auto-restore con using (Bun ≥ 1.3.9, recomendado)

import { spyOn, test, expect } from 'bun:test';

test('auto-restores spy al salir del bloque', () => {
using spy = spyOn(console, 'warn').mockImplementation(() => {});
doSomethingThatWarns();
expect(spy).toHaveBeenCalledOnce();
// spy se restaura automáticamente al salir — Symbol.dispose
});

Mock de módulos — mock.module()

Uso básico

import { mock, test, expect } from 'bun:test';

// Mockear antes de que el módulo sea importado
mock.module('./config', () => ({
default: { apiUrl: 'http://localhost:3000', timeout: 100 },
}));

// Las importaciones posteriores ven el mock
const { default: config } = await import('./config');
expect(config.apiUrl).toBe('http://localhost:3000');

Live binding update (característica única de Bun)

Bun parchea los live bindings de ES modules en tiempo de ejecución. A diferencia de Jest, el mock funciona incluso después de que el módulo ya fue importado:

import { mock, test, expect } from 'bun:test';
import { apiUrl } from './config';

// apiUrl ya está importado — en Jest esto no funcionaría
mock.module('./config', () => ({ apiUrl: 'http://mocked' }));
expect(apiUrl).toBe('http://mocked'); // ✅ live binding actualizado

Patrón recomendado: mock.module() + mockFn por test

El problema central: mock.module() no se puede deshacer dentro del mismo archivo (bun#7823, abierto desde Dic 2023). La solución: mockear la función dentro del módulo, no el módulo entero.

import { mock, beforeEach, test, expect } from 'bun:test';

// Definir el mock function una vez — esto sí se puede resetear
const fetchMock = mock(async () => ({ ok: true, json: async () => ({ id: 1 }) }));

// Mockear el módulo UNA vez apuntando al mock function
mock.module('node-fetch', () => ({ default: fetchMock }));

beforeEach(() => {
fetchMock.mockClear(); // limpiar historial de llamadas entre tests
});

test('llama a la API correctamente', async () => {
await getUser(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
});

test('maneja error de red', async () => {
fetchMock.mockRejectedValueOnce(new Error('timeout'));
await expect(getUser(1)).rejects.toThrow('timeout');
});

--preload para mocks que deben existir antes de cualquier import

Cuando un módulo tiene efectos secundarios en su inicialización y necesitas que el mock esté activo antes de que se cargue:

// test-setup.ts (declarado en bunfig.toml → [test].preload)
import { mock } from 'bun:test';

// Este mock existe antes de que cualquier test file se evalúe
const logger = { log: mock(() => {}), error: mock(() => {}) };
mock.module('./lib/logger', () => logger);

export { logger };
# bunfig.toml
[test]
preload = ["./test-setup.ts"]

Aislamiento entre archivos — --isolate y --parallel

El problema

Sin flags adicionales, los mock.module() de un archivo pueden leakear a otros archivos del mismo proceso (bun#12823).

Solución recomendada (Bun ≥ 1.3.13)

# Fresh VM context por cada archivo — mismo proceso, aislamiento garantizado
bun test --isolate

# Workers separados — aislamiento máximo, más lento
bun test --parallel

# Sharding para CI distribuido
bun test --shard=1/4

En proyectos Wiedii, añadir --isolate al target de test en project.json cuando la suite usa mock.module():

{
"targets": {
"test:unit": {
"command": "bun test --isolate src/**/*.spec.ts"
}
}
}

Timers falsos

import { test, expect, jest } from 'bun:test';

test('debounce espera 300ms', () => {
jest.useFakeTimers();

const cb = mock(() => {});
debounce(cb, 300)();

expect(cb).not.toHaveBeenCalled();
jest.advanceTimersByTime(300);
expect(cb).toHaveBeenCalledOnce();

jest.useRealTimers();
});

Alias jest.* y vi.*

bun:test exporta jest y vi como aliases completos. Útil para migrar suites existentes:

bun:test nativojest aliasvi alias
mock()jest.fn()vi.fn()
spyOn()jest.spyOn()vi.spyOn()
mock.module()jest.mock()vi.mock()
mock.restore()jest.restoreAllMocks()vi.restoreAllMocks()
mock.clearAllMocks()jest.clearAllMocks()vi.clearAllMocks()

⚠️ Gaps conocidos (mayo 2025, todos open issues)

FunciónIssueWorkaround
jest.resetModules()#5356Usar --isolate
vi.resetModules()#16140Usar --isolate
vi.unmock()#16140Separar en otro archivo
vi.importActual()#16140Spread manual en factory
jest.mock() sin factory#29834Siempre pasar factory
mock.restore() para módulos#7823Patrón mockFn + --isolate

Partial mocks — cómo imitar vi.importActual()

Como vi.importActual() no existe, el workaround es require() dentro de la factory:

mock.module('./utils', () => {
// importar el módulo real con require (síncrono) dentro de la factory
const real = require('./utils');
return {
...real, // conservar todos los exports reales
fetchData: mock(async () => []), // sobreescribir sólo lo necesario
};
});

Cobertura

# Cobertura en consola
bun test --coverage

# Cobertura en formato LCOV (para CI y merge)
bun test --coverage --coverage-reporter=lcov

# Threshold configurado en bunfig.toml (proyectos Wiedii: 80%)
# bunfig.toml
[test]
coverageThreshold = { line = 0.8, function = 0.8, statement = 0.8, branch = 0.8 }
coverageReporter = ["text", "lcov"]

Tests de propiedad con fast-check

Para tests property-based, usar la integración con @wiedii-registry/wietoo-testing:

import { describe, test, expect } from 'bun:test';
import * as fc from 'fast-check';

describe('nonEmptyArray — invariante', () => {
test('equivale a length > 0', () => {
fc.assert(
fc.property(fc.array(fc.anything()), (arr) => {
expect(isNonEmptyArray(arr)).toBe(arr.length > 0);
}),
);
});
});

Cheatsheet rápida

import {
describe, test, it, expect, // estructura
beforeAll, afterAll, // lifecycle: suite
beforeEach, afterEach, // lifecycle: por test
mock, spyOn, // mocking
jest, // alias jest.*
} from 'bun:test';

// Mock function
const fn = mock((x: number) => x * 2);
fn(3);
expect(fn).toHaveBeenCalledWith(3);
fn.mockClear(); // resetear historial
fn.mockImplementation((x) => 0); // cambiar implementación
fn.mockReturnValueOnce(99); // valor para próxima llamada

// Mock de módulo (no se puede deshacer — usar --isolate)
mock.module('pkg', () => ({ foo: 'bar' }));

// Spy con auto-restore
using spy = spyOn(obj, 'method').mockReturnValue('mocked');

// Timers
jest.useFakeTimers();
jest.advanceTimersByTime(1000);
jest.useRealTimers();

// Flags CLI clave
// bun test --isolate → fresh VM por archivo
// bun test --parallel → workers separados
// bun test --watch → re-run en cambios
// bun test --coverage → cobertura
// bun test --only → sólo tests marcados con test.only()

Por qué NO hay createModuleMocker en wietoo-devkit

Existió una utilidad createModuleMocker en @wiedii-registry/wietoo-devkit. Fue eliminada en mayo 2025 porque:

  1. Su restore() sólo reseteaba un flag interno — no restauraba el módulo real
  2. mock.module() nativo es una sola línea — el wrapper añadía ceremonial sin valor
  3. --isolate (Bun ≥ 1.3.13) resuelve el aislamiento a nivel runtime, no JS
  4. Semánticamente era incorrecto: devkit es IO helpers de producción, no test utilities

Si en el futuro se necesita una abstracción (p.ej. withAutoReset(), partial mock helpers), irá en @wiedii-registry/wietoo-testing, no en devkit.


Referencias