Skip to main content

Bun Tests

What is the Bun Test Runner?

Bun includes a native test runner that is extremely fast, built from scratch in Zig. It's Jest-compatible but significantly faster, offering a smoother development experience for JavaScript and TypeScript testing.

How Bun Tests Works

The Bun Test Runner works through an integrated system that:

  • Native Execution: Runs directly in the Bun runtime, eliminating transpilation overhead
  • Automatic Parallelization: Runs tests in parallel by default for maximum performance
  • Watch Mode: Monitors file changes and re-runs tests automatically
  • Compatible API: Uses familiar Jest APIs (describe, test, expect)
  • Native TypeScript: Built-in TypeScript support without additional configuration

Initial Setup

Installation

# Bun already includes the test runner
curl -fsSL https://bun.sh/install | bash
project/
├── src/
│ ├── utils.ts
│ ├── utils.test.ts ← Test next to file
│ └── components/
│ ├── Button.tsx
│ ├── Button.test.tsx ← Test next to component
│ ├── Modal.tsx
│ └── Modal.test.tsx
├── tests/
│ └── setup.ts ← Only global configurations
└── bun.lockb

Advantages of this structure:

  • Tests stay close to the code they test
  • Easier maintenance and test discovery
  • Avoids duplicating folder structure
  • Better organization for large projects

How to Create Tests

Basic Test Structure

import { describe, test, expect } from "bun:test";

describe("My functionality", () => {
test("should do something specific", () => {
// Arrange (Setup)
const input = "test";

// Act (Execute)
const result = myFunction(input);

// Assert (Verify)
expect(result).toBe("TEST");
});
});

Practical Example: Testing a Utility Function

File: src/math.ts

export function add(a: number, b: number): number {
return a + b;
}

export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Division by zero is not allowed");
}
return a / b;
}

export function isEven(number: number): boolean {
return number % 2 === 0;
}

File: src/math.test.ts

import { describe, test, expect } from "bun:test";
import { add, divide, isEven } from "./math";

describe("Math functions", () => {
describe("add", () => {
test("should add two positive numbers", () => {
expect(add(2, 3)).toBe(5);
});

test("should add negative numbers", () => {
expect(add(-2, -3)).toBe(-5);
});

test("should add zero", () => {
expect(add(5, 0)).toBe(5);
});
});

describe("divide", () => {
test("should divide numbers normally", () => {
expect(divide(10, 2)).toBe(5);
});

test("should throw error when dividing by zero", () => {
expect(() => divide(10, 0)).toThrow("Division by zero is not allowed");
});
});

describe("isEven", () => {
test("should return true for even numbers", () => {
expect(isEven(4)).toBe(true);
expect(isEven(0)).toBe(true);
});

test("should return false for odd numbers", () => {
expect(isEven(3)).toBe(false);
expect(isEven(7)).toBe(false);
});
});
});

Async Tests

import { describe, test, expect } from "bun:test";

describe("Async operations", () => {
test("should fetch data from API", async () => {
const response = await fetch("https://api.example.com/data");
const data = await response.json();

expect(response.status).toBe(200);
expect(data).toHaveProperty("id");
});

test("should wait for timeout", async () => {
const promise = new Promise(resolve => {
setTimeout(() => resolve("completed"), 100);
});

const result = await promise;
expect(result).toBe("completed");
});
});

Setting up for Hooks and Component Testing

To test React hooks or components that interact with the DOM, you need to set up an appropriate testing environment.

Test Environment Setup

File: tests/setup.ts

import '@testing-library/jest-dom'
import { beforeEach } from 'bun:test'
import { JSDOM } from 'jsdom'

// Setup JSDOM to simulate browser environment
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
url: 'http://localhost',
pretendToBeVisual: true,
})

const { window } = dom
const { document } = window

// Make browser globals available
global.window = window as any
global.document = document
global.navigator = window.navigator as any
global.HTMLElement = window.HTMLElement
global.Element = window.Element
global.Node = window.Node

// Mock APIs that might not exist in JSDOM
global.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
}

// Mock localStorage if needed
global.localStorage = {
getItem: (key: string) => null,
setItem: (key: string, value: string) => {},
removeItem: (key: string) => {},
clear: () => {},
length: 0,
key: (index: number) => null
}

// Clean DOM between tests
beforeEach(() => {
document.head.innerHTML = ''
document.body.innerHTML = ''
})

console.log('JSDOM - document defined:', typeof document !== 'undefined')

Bun Configuration to Load Setup

File: bunfig.toml

[test]
preload = ["./tests/setup.ts"]

Or via command line:

bun test --preload ./tests/setup.ts

Required Dependencies

package.json

{
"devDependencies": {
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^14.1.2",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.1",
"jsdom": "^23.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}

Testing React Hooks

Simple Hook - useState

File: src/hooks/useCounter.ts

import { useState } from 'react';

export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);

const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
const reset = () => setCount(initialValue);

return {
count,
increment,
decrement,
reset
};
}

File: src/hooks/useCounter.test.tsx

import { describe, test, expect } from 'bun:test';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

describe('useCounter', () => {
test('should initialize with default value', () => {
const { result } = renderHook(() => useCounter());

expect(result.current.count).toBe(0);
});

test('should initialize with custom value', () => {
const { result } = renderHook(() => useCounter(10));

expect(result.current.count).toBe(10);
});

test('should increment counter', () => {
const { result } = renderHook(() => useCounter());

act(() => {
result.current.increment();
});

expect(result.current.count).toBe(1);
});

test('should decrement counter', () => {
const { result } = renderHook(() => useCounter(5));

act(() => {
result.current.decrement();
});

expect(result.current.count).toBe(4);
});

test('should reset to initial value', () => {
const { result } = renderHook(() => useCounter(10));

act(() => {
result.current.increment();
result.current.increment();
});

expect(result.current.count).toBe(12);

act(() => {
result.current.reset();
});

expect(result.current.count).toBe(10);
});
});

Testing React Components

Simple Component

File: src/components/Button.tsx

interface ButtonProps {
onClick: () => void;
disabled?: boolean;
children: React.ReactNode;
}

export function Button({ onClick, disabled = false, children }: ButtonProps) {
return (
<button
onClick={onClick}
disabled={disabled}
className={`btn ${disabled ? 'btn-disabled' : 'btn-primary'}`}
>
{children}
</button>
);
}

File: src/components/Button.test.tsx

import { describe, test, expect } from 'bun:test';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from './Button';

describe('Button', () => {
test('should render with correct text', () => {
render(<Button onClick={() => {}}>Click me</Button>);

expect(screen.getByText('Click me')).toBeInTheDocument();
});

test('should call onClick when clicked', async () => {
const user = userEvent.setup();
const handleClick = () => {};
const spy = { calls: 0 };
const mockOnClick = () => spy.calls++;

render(<Button onClick={mockOnClick}>Click me</Button>);

await user.click(screen.getByText('Click me'));

expect(spy.calls).toBe(1);
});

test('should be disabled when disabled is true', () => {
render(<Button onClick={() => {}} disabled>Click me</Button>);

expect(screen.getByText('Click me')).toBeDisabled();
});

test('should apply correct CSS class when disabled', () => {
render(<Button onClick={() => {}} disabled>Click me</Button>);

expect(screen.getByText('Click me')).toHaveClass('btn-disabled');
});
});

How to Run Tests

Basic Commands

# Run all tests
bun test

# Run tests in watch mode (observe changes)
bun test --watch

# Run specific tests by name pattern
bun test math

# Run with coverage
bun test --coverage

# Run with verbose output
bun test --verbose

Advanced Configuration

File: bunfig.toml

[test]
# Default timeout for tests (in ms)
timeout = 5000

# Test directories
testdir = ["tests", "src/**/*.test.ts"]

# Ignore files
ignore = ["node_modules", "dist"]

Scripts in package.json

{
"scripts": {
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage",
"test:ci": "bun test --reporter=junit",
"test:hooks": "bun test --preload ./tests/setup.ts"
}
}

When Creating Tests is Necessary

ALWAYS TEST:

  1. Critical Business Logic

    // Financial calculations, important validations
    function calculateInterest(principal: number, rate: number, time: number) {
    return principal * Math.pow(1 + rate, time);
    }
  2. Pure Functions and Utilities

    // Easy to test, high reliability
    function formatCPF(cpf: string): string {
    return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
    }
  3. APIs and Endpoints

    // Important contracts with frontend
    export async function createUser(userData: UserDTO) {
    // creation logic
    }
  4. Data Validations and Transformations

    function validateEmail(email: string): boolean {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
    }

⚠️ CONSIDER TESTING:

  1. Components with Complex Logic
  2. Integrations with External Services
  3. Custom Algorithms and Data Structures
  4. Reused Logic in Many Places
    // Function that validates forms throughout the application
    function validateForm(data: FormData): ValidationResult {
    // critical logic that needs to work the same way always
    }
  5. Base Design System Components
    // Component used in dozens of screens
    function InputField({ validation, format, mask }: InputProps) {
    // behavior must be consistent throughout the app
    }

NOT NECESSARY TO TEST:

  1. Simple Configurations

    // Just exports constants
    export const API_BASE_URL = "https://api.example.com";
  2. Trivial Code

    // Simple getters/setters
    get name() { return this._name; }
  3. Third-party Libraries

    // Lodash, etc. are already tested
    import { debounce } from "lodash";
  4. Pure Presentation Code

    // HTML/CSS without logic
    function renderTemplate(data: any) {
    return `<h1>${data.title}</h1>`;
    }

Why Tests Are Important

🛡️ Security and Reliability

  • Detect bugs before production
  • Prevent regressions in future changes
  • Ensure code works as expected

🚀 Development Productivity

  • Enable confident refactoring
  • Serve as living documentation of code
  • Reduce debugging time

Best Practices

Clear Naming

// ❌ Bad
test("test1", () => {});

// ✅ Good
test("should return error when CPF is invalid", () => {});

Arrange, Act, Assert

test("should calculate discount correctly", () => {
// Arrange - prepare data
const originalPrice = 100;
const discountPercentage = 10;

// Act - execute action
const discountedPrice = applyDiscount(originalPrice, discountPercentage);

// Assert - verify result
expect(discountedPrice).toBe(90);
});

One Concept per Test

// ❌ Test doing too many things
test("user validation", () => {
expect(validateEmail("test@test.com")).toBe(true);
expect(validateCPF("12345678901")).toBe(true);
expect(validatePhone("11999999999")).toBe(true);
});

// ✅ Focused tests
describe("Validations", () => {
test("should validate correct email", () => {
expect(validateEmail("test@test.com")).toBe(true);
});

test("should validate correct CPF", () => {
expect(validateCPF("12345678901")).toBe(true);
});
});

Complete Example: User System

File: src/user.ts

export interface User {
id: number;
name: string;
email: string;
active: boolean;
}

export class UserManager {
private users: User[] = [];
private nextId = 1;

createUser(name: string, email: string): User {
if (!name.trim()) {
throw new Error("Name is required");
}

if (!this.validateEmail(email)) {
throw new Error("Invalid email");
}

const user: User = {
id: this.nextId++,
name: name.trim(),
email: email.toLowerCase(),
active: true
};

this.users.push(user);
return user;
}

findByEmail(email: string): User | undefined {
return this.users.find(u => u.email === email.toLowerCase());
}

deactivateUser(id: number): boolean {
const user = this.users.find(u => u.id === id);
if (user) {
user.active = false;
return true;
}
return false;
}

private validateEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}

File: src/user.test.ts

import { describe, test, expect, beforeEach } from "bun:test";
import { UserManager } from "./user";

describe("UserManager", () => {
let manager: UserManager;

beforeEach(() => {
manager = new UserManager();
});

describe("createUser", () => {
test("should create user with valid data", () => {
const user = manager.createUser("John Smith", "john@test.com");

expect(user.id).toBe(1);
expect(user.name).toBe("John Smith");
expect(user.email).toBe("john@test.com");
expect(user.active).toBe(true);
});

test("should normalize email to lowercase", () => {
const user = manager.createUser("John", "JOHN@TEST.COM");
expect(user.email).toBe("john@test.com");
});

test("should trim name spaces", () => {
const user = manager.createUser(" John Smith ", "john@test.com");
expect(user.name).toBe("John Smith");
});

test("should throw error for empty name", () => {
expect(() => {
manager.createUser("", "john@test.com");
}).toThrow("Name is required");
});

test("should throw error for invalid email", () => {
expect(() => {
manager.createUser("John", "invalid-email");
}).toThrow("Invalid email");
});
});

describe("findByEmail", () => {
test("should find existing user", () => {
manager.createUser("John", "john@test.com");
const user = manager.findByEmail("john@test.com");

expect(user).toBeDefined();
expect(user?.name).toBe("John");
});

test("should be case-insensitive", () => {
manager.createUser("John", "john@test.com");
const user = manager.findByEmail("JOHN@TEST.COM");

expect(user).toBeDefined();
});

test("should return undefined for non-existent user", () => {
const user = manager.findByEmail("nonexistent@test.com");
expect(user).toBeUndefined();
});
});

describe("deactivateUser", () => {
test("should deactivate existing user", () => {
const user = manager.createUser("John", "john@test.com");
const result = manager.deactivateUser(user.id);

expect(result).toBe(true);
expect(user.active).toBe(false);
});

test("should return false for non-existent user", () => {
const result = manager.deactivateUser(999);
expect(result).toBe(false);
});
});
});

Conclusion

The Bun Test Runner offers a superior testing experience with exceptional performance and minimal configuration. Remember: test what matters, keep tests simple and clear, and use them as a tool to build more reliable and maintainable software.

With this approach, you'll have a solid foundation for implementing effective tests in your projects using Bun.