--- name: Bun Test Runner description: Fast test execution with Bun's built-in test runner including snapshot testing, mocking, code coverage, lifecycle hooks, DOM testing with happy-dom, and migration from Jest and Vitest to Bun test. version: 1.0.0 author: thetestingacademy license: MIT tags: [bun, bun-test, test-runner, snapshot-testing, mocking, code-coverage, happy-dom, fast-testing, runtime, javascript-runtime] testingTypes: [unit, integration] frameworks: [bun] languages: [typescript, javascript] domains: [backend, web, api] agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt, gemini-cli, amp] --- # Bun Test Runner Skill You are an expert in Bun's built-in test runner. When the user asks you to write tests using Bun, migrate from Jest or Vitest to Bun test, configure code coverage, or optimize test execution speed, follow these detailed instructions. ## Core Principles 1. **Zero-config test runner** -- Bun's test runner works out of the box with no configuration files. Tests are discovered automatically by filename patterns. 2. **Jest-compatible API** -- Bun test provides a Jest-compatible API with describe, it, expect, and lifecycle hooks. Migration from Jest is straightforward. 3. **Native TypeScript support** -- Bun executes TypeScript directly without transpilation. No ts-jest or tsconfig paths configuration needed. 4. **Built-in mocking** -- Use bun:test's mock, spyOn, and module mocking capabilities without installing separate packages. 5. **Snapshot testing** -- Bun supports snapshot testing with toMatchSnapshot() and inline snapshots, compatible with Jest snapshot format. 6. **Code coverage** -- Generate code coverage reports with --coverage flag. No additional tools like c8 or istanbul needed. 7. **Parallel by default** -- Bun runs test files in parallel by default. Design tests to be independent for correct parallel execution. ## Project Structure ``` src/ utils/ math.ts math.test.ts string.ts string.test.ts services/ user-service.ts user-service.test.ts api-client.ts api-client.test.ts db/ queries.ts queries.test.ts __snapshots__/ .gitkeep bunfig.toml package.json ``` ## Bun Configuration ```toml # bunfig.toml [test] # Test file patterns root = "./src" # Coverage configuration coverage = true coverageReporter = ["text", "lcov"] coverageThreshold = { line = 80, function = 80, statement = 80 } # Preload scripts preload = ["./test-setup.ts"] # Timeout per test (ms) timeout = 5000 # Bail after N failures (0 = no bail) bail = 0 ``` ## Basic Test Patterns ```typescript // src/utils/math.test.ts import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; import { add, multiply, divide, fibonacci, isPrime } from './math'; describe('Math Utilities', () => { describe('add', () => { it('should add two positive numbers', () => { expect(add(2, 3)).toBe(5); }); it('should handle negative numbers', () => { expect(add(-1, -2)).toBe(-3); expect(add(-1, 5)).toBe(4); }); it('should handle zero', () => { expect(add(0, 0)).toBe(0); expect(add(5, 0)).toBe(5); }); it('should handle floating point', () => { expect(add(0.1, 0.2)).toBeCloseTo(0.3); }); }); describe('divide', () => { it('should divide two numbers', () => { expect(divide(10, 2)).toBe(5); }); it('should throw on division by zero', () => { expect(() => divide(10, 0)).toThrow('Division by zero'); }); it('should handle decimal results', () => { expect(divide(1, 3)).toBeCloseTo(0.333, 2); }); }); describe('fibonacci', () => { it('should return correct values for small inputs', () => { expect(fibonacci(0)).toBe(0); expect(fibonacci(1)).toBe(1); expect(fibonacci(2)).toBe(1); expect(fibonacci(10)).toBe(55); }); it('should throw for negative inputs', () => { expect(() => fibonacci(-1)).toThrow(); }); }); describe('isPrime', () => { it.each([2, 3, 5, 7, 11, 13])('should identify %d as prime', (n) => { expect(isPrime(n)).toBe(true); }); it.each([0, 1, 4, 6, 8, 9, 10])('should identify %d as not prime', (n) => { expect(isPrime(n)).toBe(false); }); }); }); ``` ## Mocking with Bun ```typescript // src/services/user-service.test.ts import { describe, it, expect, mock, spyOn, beforeEach } from 'bun:test'; import { UserService } from './user-service'; import { db } from '../db/connection'; // Mock the entire module mock.module('../db/connection', () => ({ db: { query: mock(() => Promise.resolve([])), insert: mock(() => Promise.resolve({ id: '123' })), update: mock(() => Promise.resolve({ affected: 1 })), delete: mock(() => Promise.resolve({ affected: 1 })), }, })); describe('UserService', () => { let service: UserService; beforeEach(() => { service = new UserService(); // Reset all mocks (db.query as any).mockClear(); (db.insert as any).mockClear(); }); it('should find a user by ID', async () => { const mockUser = { id: '123', name: 'Alice', email: 'alice@test.com' }; (db.query as any).mockResolvedValueOnce([mockUser]); const user = await service.findById('123'); expect(user).toEqual(mockUser); expect(db.query).toHaveBeenCalledTimes(1); }); it('should return null for non-existent user', async () => { (db.query as any).mockResolvedValueOnce([]); const user = await service.findById('nonexistent'); expect(user).toBeNull(); }); it('should create a new user', async () => { const newUser = { name: 'Bob', email: 'bob@test.com' }; (db.insert as any).mockResolvedValueOnce({ id: '456', ...newUser }); const created = await service.create(newUser); expect(created.id).toBe('456'); expect(db.insert).toHaveBeenCalledTimes(1); }); it('should throw on duplicate email', async () => { (db.insert as any).mockRejectedValueOnce(new Error('UNIQUE constraint failed')); expect(service.create({ name: 'Bob', email: 'existing@test.com' })).rejects.toThrow( 'Email already exists' ); }); }); ``` ## Spy Functions ```typescript // src/services/api-client.test.ts import { describe, it, expect, spyOn, mock, beforeEach, afterEach } from 'bun:test'; import { ApiClient } from './api-client'; describe('ApiClient', () => { let client: ApiClient; let fetchSpy: any; beforeEach(() => { client = new ApiClient('https://api.example.com'); fetchSpy = spyOn(globalThis, 'fetch'); }); afterEach(() => { fetchSpy.mockRestore(); }); it('should make GET requests', async () => { fetchSpy.mockResolvedValueOnce( new Response(JSON.stringify({ data: 'test' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }) ); const result = await client.get('/users'); expect(result).toEqual({ data: 'test' }); expect(fetchSpy).toHaveBeenCalledWith( 'https://api.example.com/users', expect.objectContaining({ method: 'GET' }) ); }); it('should handle network errors', async () => { fetchSpy.mockRejectedValueOnce(new Error('Network error')); expect(client.get('/users')).rejects.toThrow('Network error'); }); it('should retry on 5xx errors', async () => { fetchSpy .mockResolvedValueOnce(new Response('', { status: 503 })) .mockResolvedValueOnce(new Response('', { status: 503 })) .mockResolvedValueOnce( new Response(JSON.stringify({ data: 'ok' }), { status: 200 }) ); const result = await client.get('/users', { retries: 3 }); expect(result).toEqual({ data: 'ok' }); expect(fetchSpy).toHaveBeenCalledTimes(3); }); it('should include authorization header', async () => { fetchSpy.mockResolvedValueOnce( new Response(JSON.stringify({}), { status: 200 }) ); client.setToken('test-token'); await client.get('/protected'); expect(fetchSpy).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ headers: expect.objectContaining({ Authorization: 'Bearer test-token', }), }) ); }); }); ``` ## Snapshot Testing ```typescript // src/utils/string.test.ts import { describe, it, expect } from 'bun:test'; import { formatDate, slugify, truncate, parseMarkdown } from './string'; describe('String Utilities', () => { describe('slugify', () => { it('should create URL-safe slugs', () => { expect(slugify('Hello World')).toBe('hello-world'); expect(slugify(' Multiple Spaces ')).toBe('multiple-spaces'); expect(slugify('Special @#$ Characters!')).toBe('special-characters'); expect(slugify('CamelCaseTitle')).toBe('camelcasetitle'); }); it('should match snapshot', () => { const testCases = [ 'Hello World', 'TypeScript Testing', 'API v2.0 Documentation', 'user@email.com', ].map((input) => ({ input, output: slugify(input) })); expect(testCases).toMatchSnapshot(); }); }); describe('parseMarkdown', () => { it('should parse headings', () => { expect(parseMarkdown('# Title')).toMatchSnapshot(); }); it('should parse lists', () => { const input = '- Item 1\n- Item 2\n- Item 3'; expect(parseMarkdown(input)).toMatchSnapshot(); }); it('should parse code blocks', () => { const input = '```typescript\nconst x = 1;\n```'; expect(parseMarkdown(input)).toMatchSnapshot(); }); }); }); ``` ## DOM Testing with happy-dom ```typescript // test-setup.ts import { GlobalRegistrator } from '@happy-dom/global-registrator'; GlobalRegistrator.register(); ``` ```typescript // src/components/counter.test.ts import { describe, it, expect, beforeEach } from 'bun:test'; describe('Counter Component (DOM)', () => { beforeEach(() => { document.body.innerHTML = ''; }); it('should render counter with initial value', () => { document.body.innerHTML = `