# Testing with bun:test Bun's built-in test runner patterns and lifecycle hooks. ## Test Structure ```typescript import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from 'bun:test'; describe('feature', () => { let resource: Resource; beforeAll(() => { // Suite setup — runs once before all tests console.log('Setup test suite'); }); afterAll(() => { // Suite cleanup — runs once after all tests console.log('Cleanup test suite'); }); beforeEach(() => { // Test setup — runs before each test resource = createResource(); }); afterEach(() => { // Test cleanup — runs after each test resource.dispose(); }); test('behavior', () => { expect(result).toBe(expected); }); }); ``` ## Assertions ```typescript // Equality expect(value).toBe(expected); // Strict equality (===) expect(obj).toEqual({ foo: 'bar' }); // Deep equality expect(arr).toContain(item); // Array/string contains expect(obj).toMatchObject({ key: 'value' }); // Partial object match // Truthiness expect(value).toBeTruthy(); expect(value).toBeFalsy(); expect(value).toBeDefined(); expect(value).toBeUndefined(); expect(value).toBeNull(); // Numbers expect(num).toBeGreaterThan(0); expect(num).toBeGreaterThanOrEqual(0); expect(num).toBeLessThan(100); expect(num).toBeLessThanOrEqual(100); expect(num).toBeCloseTo(0.3, 5); // Float comparison // Strings expect(str).toMatch(/pattern/); expect(str).toStartWith('prefix'); expect(str).toEndWith('suffix'); // Arrays expect(arr).toHaveLength(3); expect(arr).toContainEqual({ id: 1 }); // Exceptions expect(fn).toThrow(); expect(fn).toThrow('error message'); expect(fn).toThrow(ErrorType); // Negation expect(value).not.toBe(other); expect(arr).not.toContain(item); ``` ## Async Tests ```typescript // Async/await test('async operation', async () => { const result = await fetchData(); expect(result).toBeDefined(); }); // Promise resolution test('promise resolves', async () => { await expect(asyncFn()).resolves.toBe('success'); }); // Promise rejection test('promise rejects', async () => { await expect(asyncFn()).rejects.toThrow('error'); }); // Timeout (default 5000ms) test('slow operation', async () => { const result = await slowOperation(); expect(result).toBeDefined(); }, 10000); // 10 second timeout ``` ## Database Testing ```typescript import { Database } from 'bun:sqlite'; describe('Database operations', () => { let db: Database; beforeEach(() => { // Fresh in-memory database per test db = new Database(':memory:'); db.run(` CREATE TABLE users ( id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL, name TEXT NOT NULL ) `); }); afterEach(() => { db.close(); }); test('insert user', () => { const user = db.prepare(` INSERT INTO users (id, email, name) VALUES (?, ?, ?) RETURNING * `).get('1', 'alice@example.com', 'Alice'); expect(user).toMatchObject({ id: '1', email: 'alice@example.com', name: 'Alice' }); }); test('query user', () => { db.run("INSERT INTO users VALUES ('1', 'alice@example.com', 'Alice')"); const user = db.prepare('SELECT * FROM users WHERE id = ?').get('1'); expect(user).toBeDefined(); expect(user.email).toBe('alice@example.com'); }); test('unique constraint', () => { db.run("INSERT INTO users VALUES ('1', 'alice@example.com', 'Alice')"); expect(() => { db.run("INSERT INTO users VALUES ('2', 'alice@example.com', 'Alice2')"); }).toThrow(); }); }); ``` ## File System Testing ```typescript import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; describe('File operations', () => { let tempDir: string; beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), 'test-')); }); afterEach(async () => { await rm(tempDir, { recursive: true }); }); test('write and read file', async () => { const filepath = join(tempDir, 'test.txt'); await Bun.write(filepath, 'Hello, world!'); const file = Bun.file(filepath); expect(await file.exists()).toBe(true); expect(await file.text()).toBe('Hello, world!'); }); test('write JSON', async () => { const filepath = join(tempDir, 'data.json'); const data = { name: 'test', value: 42 }; await Bun.write(filepath, JSON.stringify(data)); const file = Bun.file(filepath); expect(await file.json()).toEqual(data); }); }); ``` ## Mocking ```typescript import { mock, spyOn } from 'bun:test'; describe('Mocking', () => { test('mock function', () => { const mockFn = mock(() => 'mocked'); expect(mockFn()).toBe('mocked'); expect(mockFn).toHaveBeenCalled(); expect(mockFn).toHaveBeenCalledTimes(1); }); test('mock with arguments', () => { const mockFn = mock((x: number) => x * 2); mockFn(5); expect(mockFn).toHaveBeenCalledWith(5); }); test('spy on method', () => { const obj = { method: (x: number) => x * 2 }; const spy = spyOn(obj, 'method'); obj.method(5); expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledWith(5); }); test('mock return value', () => { const mockFn = mock(() => 'original'); mockFn.mockReturnValue('mocked'); expect(mockFn()).toBe('mocked'); mockFn.mockReturnValueOnce('once'); expect(mockFn()).toBe('once'); expect(mockFn()).toBe('mocked'); }); test('mock implementation', () => { const mockFn = mock(() => 'original'); mockFn.mockImplementation(() => 'new implementation'); expect(mockFn()).toBe('new implementation'); }); }); ``` ## Mock fetch ```typescript describe('External API calls', () => { const originalFetch = global.fetch; afterEach(() => { global.fetch = originalFetch; }); test('mock API response', async () => { global.fetch = mock(async () => new Response(JSON.stringify({ data: 'mocked' }), { status: 200, headers: { 'Content-Type': 'application/json' } }) ); const res = await fetch('https://api.example.com/data'); const data = await res.json(); expect(data).toEqual({ data: 'mocked' }); expect(global.fetch).toHaveBeenCalledWith('https://api.example.com/data'); }); test('mock API error', async () => { global.fetch = mock(async () => new Response(JSON.stringify({ error: 'Not found' }), { status: 404 }) ); const res = await fetch('https://api.example.com/missing'); expect(res.status).toBe(404); }); }); ``` ## Test Organization ```typescript // Skip tests test.skip('work in progress', () => { // Not executed }); // Mark as todo test.todo('future feature'); // Only run specific test test.only('focus on this', () => { // Only this test runs in file }); // Conditional skip const isCI = process.env.CI === 'true'; test.skipIf(isCI)('skip in CI', () => { // Skipped when CI=true }); // Run if condition test.if(!isCI)('local only', () => { // Only runs locally }); ``` ## Snapshot Testing ```typescript import { expect, test } from 'bun:test'; test('snapshot', () => { const result = generateOutput(); expect(result).toMatchSnapshot(); }); test('inline snapshot', () => { const result = { name: 'test', value: 42 }; expect(result).toMatchInlineSnapshot(` { "name": "test", "value": 42 } `); }); ``` ## Running Tests ```bash # Run all tests bun test # Specific file bun test src/utils.test.ts # Specific directory bun test src/api/ # Pattern matching bun test --test-name-pattern "should create" # Watch mode bun test --watch # Coverage bun test --coverage # Timeout (ms) bun test --timeout 10000 # Bail on first failure bun test --bail # Rerun only failed tests bun test --rerun-each 3 ``` ## Best Practices ```typescript // ✅ Isolated tests — each test sets up its own data describe('User service', () => { let db: Database; beforeEach(() => { db = new Database(':memory:'); setupSchema(db); }); afterEach(() => { db.close(); }); test('creates user', () => { const user = createUser(db, { email: 'test@example.com' }); expect(user.id).toBeDefined(); }); }); // ✅ Descriptive test names test('returns 404 when user not found', async () => { ... }); test('validates email format before creating user', async () => { ... }); // ✅ Single assertion focus test('user has correct email', () => { const user = createUser({ email: 'test@example.com' }); expect(user.email).toBe('test@example.com'); }); // ❌ Avoid shared mutable state between tests let sharedUser; // Don't do this beforeAll(() => { sharedUser = createUser(); // Tests may interfere }); ```