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
Recommended Project Structure
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:
-
Critical Business Logic
// Financial calculations, important validations
function calculateInterest(principal: number, rate: number, time: number) {
return principal * Math.pow(1 + rate, time);
} -
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');
} -
APIs and Endpoints
// Important contracts with frontend
export async function createUser(userData: UserDTO) {
// creation logic
} -
Data Validations and Transformations
function validateEmail(email: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
⚠️ CONSIDER TESTING:
- Components with Complex Logic
- Integrations with External Services
- Custom Algorithms and Data Structures
- 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
} - 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:
-
Simple Configurations
// Just exports constants
export const API_BASE_URL = "https://api.example.com"; -
Trivial Code
// Simple getters/setters
get name() { return this._name; } -
Third-party Libraries
// Lodash, etc. are already tested
import { debounce } from "lodash"; -
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.