Skip to content

Bun Test Runner

Bun has a built-in fast test runner that is compatible with Jest syntax, supports TypeScript, and requires no additional configuration. This chapter introduces the usage of Bun testing.

Quick Start

Creating a Test File

typescript
// math.test.ts
import { expect, test } from "bun:test";

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

test("addition test", () => {
  expect(add(2, 3)).toBe(5);
});

Running Tests

bash
# Run all tests
bun test

# Run specific file
bun test math.test.ts

# Run matching test files
bun test --pattern "*.spec.ts"

# Watch mode
bun test --watch

Test Syntax

test Function

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

// Basic test
test("basic test", () => {
  expect(1 + 1).toBe(2);
});

// Async test
test("async test", async () => {
  const result = await Promise.resolve(42);
  expect(result).toBe(42);
});

// Skip test
test.skip("skipped test", () => {
  // Will not execute
});

// Only run this test
test.only("only run this", () => {
  expect(true).toBe(true);
});

// Todo test
test.todo("feature to be implemented");

// Test with timeout
test("timeout test", async () => {
  await Bun.sleep(100);
}, { timeout: 5000 });

describe Grouping

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

describe("math operations", () => {
  test("addition", () => {
    expect(1 + 1).toBe(2);
  });
  
  test("subtraction", () => {
    expect(5 - 3).toBe(2);
  });
  
  describe("advanced operations", () => {
    test("multiplication", () => {
      expect(2 * 3).toBe(6);
    });
    
    test("division", () => {
      expect(6 / 2).toBe(3);
    });
  });
});

Lifecycle Hooks

typescript
import { describe, test, expect, beforeAll, afterAll, beforeEach, afterEach } from "bun:test";

describe("lifecycle test", () => {
  let db: Database;
  
  beforeAll(async () => {
    // Execute once before all tests
    db = await Database.connect();
    console.log("Database connected");
  });
  
  afterAll(async () => {
    // Execute once after all tests
    await db.close();
    console.log("Database closed");
  });
  
  beforeEach(() => {
    // Execute before each test
    console.log("Preparing test data");
  });
  
  afterEach(() => {
    // Execute after each test
    console.log("Cleaning up test data");
  });
  
  test("test 1", () => {
    expect(db.isConnected()).toBe(true);
  });
  
  test("test 2", () => {
    expect(db.isConnected()).toBe(true);
  });
});

Assertion Methods

Basic Assertions

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

test("basic assertions", () => {
  // Equality
  expect(1 + 1).toBe(2);
  expect({ a: 1 }).toEqual({ a: 1 });
  
  // Inequality
  expect(1).not.toBe(2);
  
  // Truthy/falsy
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
  expect(null).toBeNull();
  expect(undefined).toBeUndefined();
  expect("hello").toBeDefined();
  
  // Type checking
  expect(typeof "hello").toBe("string");
  expect([]).toBeInstanceOf(Array);
});

Number Assertions

typescript
test("number assertions", () => {
  expect(10).toBeGreaterThan(5);
  expect(10).toBeGreaterThanOrEqual(10);
  expect(5).toBeLessThan(10);
  expect(5).toBeLessThanOrEqual(5);
  
  // Floating point comparison
  expect(0.1 + 0.2).toBeCloseTo(0.3);
  expect(0.1 + 0.2).toBeCloseTo(0.3, 5); // 5 digits precision
  
  // NaN check
  expect(NaN).toBeNaN();
});

String Assertions

typescript
test("string assertions", () => {
  expect("hello world").toContain("world");
  expect("hello world").toMatch(/hello/);
  expect("hello world").toStartWith("hello");
  expect("hello world").toEndWith("world");
  expect("hello").toHaveLength(5);
});

Array Assertions

typescript
test("array assertions", () => {
  const arr = [1, 2, 3];
  
  expect(arr).toContain(2);
  expect(arr).toHaveLength(3);
  expect(arr).toEqual([1, 2, 3]);
  
  // Array containing objects
  const users = [{ id: 1, name: "张三" }];
  expect(users).toContainEqual({ id: 1, name: "张三" });
});

Object Assertions

typescript
test("object assertions", () => {
  const obj = { a: 1, b: 2, c: { d: 3 } };
  
  expect(obj).toEqual({ a: 1, b: 2, c: { d: 3 } });
  expect(obj).toMatchObject({ a: 1, b: 2 });
  expect(obj).toHaveProperty("a");
  expect(obj).toHaveProperty("a", 1);
  expect(obj).toHaveProperty("c.d", 3);
});

Exception Assertions

typescript
test("exception assertions", () => {
  function throwError() {
    throw new Error("Error occurred");
  }
  
  expect(throwError).toThrow();
  expect(throwError).toThrow("Error occurred");
  expect(throwError).toThrow(/Error/);
  expect(throwError).toThrow(Error);
});

test("async exception", async () => {
  async function asyncThrow() {
    throw new Error("Async error");
  }
  
  expect(asyncThrow()).rejects.toThrow("Async error");
});

Mock Functions

Mock Functions

typescript
import { test, expect, mock } from "bun:test";

test("mock function", () => {
  // Create mock function
  const fn = mock(() => 42);
  
  // Call
  expect(fn()).toBe(42);
  expect(fn(1, 2)).toBe(42);
  
  // Verify calls
  expect(fn).toHaveBeenCalled();
  expect(fn).toHaveBeenCalledTimes(2);
  expect(fn).toHaveBeenCalledWith(1, 2);
});

test("mock implementation", () => {
  const fn = mock();
  
  // Set return value
  fn.mockReturnValue(100);
  expect(fn()).toBe(100);
  
  // Set one-time return value
  fn.mockReturnValueOnce(1);
  fn.mockReturnValueOnce(2);
  expect(fn()).toBe(1);
  expect(fn()).toBe(2);
  expect(fn()).toBe(100);
  
  // Set implementation
  fn.mockImplementation((a, b) => a + b);
  expect(fn(2, 3)).toBe(5);
});

Spy Functions

typescript
import { test, expect, spyOn } from "bun:test";

test("spy function", () => {
  const obj = {
    greet(name: string) {
      return `Hello, ${name}!`;
    },
  };
  
  // Spy on method
  const spy = spyOn(obj, "greet");
  
  // Call original method
  expect(obj.greet("Bun")).toBe("Hello, Bun!");
  
  // Verify calls
  expect(spy).toHaveBeenCalled();
  expect(spy).toHaveBeenCalledWith("Bun");
  
  // Restore original method
  spy.mockRestore();
});

Mock Modules

typescript
import { test, expect, mock } from "bun:test";

// Mock entire module
mock.module("./api", () => ({
  fetchUsers: mock(() => Promise.resolve([{ id: 1, name: "张三" }])),
  fetchPosts: mock(() => Promise.resolve([])),
}));

// Now importing mock version
import { fetchUsers, fetchPosts } from "./api";

test("mock module", async () => {
  const users = await fetchUsers();
  expect(users).toHaveLength(1);
  expect(users[0].name).toBe("张三");
});

Snapshot Testing

Basic Snapshots

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

test("snapshot test", () => {
  const user = {
    id: 1,
    name: "张三",
    email: "zhangsan@example.com",
    createdAt: new Date("2024-01-01"),
  };
  
  expect(user).toMatchSnapshot();
});

Updating Snapshots

bash
# Update all snapshots
bun test --update-snapshots

# Short form
bun test -u

Inline Snapshots

typescript
test("inline snapshot", () => {
  const result = { a: 1, b: 2 };
  
  expect(result).toMatchInlineSnapshot(`
    {
      "a": 1,
      "b": 2,
    }
  `);
});

Code Coverage

Enable Coverage

bash
# Run tests and generate coverage report
bun test --coverage

Coverage Output

----------|---------|----------|---------|---------|
File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
All files |   85.71 |    75.00 |   66.67 |   85.71 |
math.ts   |  100.00 |   100.00 |  100.00 |  100.00 |
utils.ts  |   75.00 |    50.00 |   50.00 |   75.00 |
----------|---------|----------|---------|---------|

Coverage Configuration

toml
# bunfig.toml
[test]
coverage = true
coverageThreshold = 80

# Ignored files
coverageIgnorePatterns = [
  "node_modules",
  "test",
  "*.config.*",
]

Test Configuration

bunfig.toml Configuration

toml
[test]
# Test file matching
testMatch = ["**/*.test.ts", "**/*.spec.ts"]

# Timeout (milliseconds)
timeout = 5000

# Preload script
preload = ["./test/setup.ts"]

# Enable coverage
coverage = true

# Coverage threshold
coverageThreshold = 80

Test Setup File

typescript
// test/setup.ts
import { beforeAll, afterAll } from "bun:test";

// Global setup
beforeAll(() => {
  console.log("Starting tests...");
});

afterAll(() => {
  console.log("Tests complete!");
});

// Global mock
global.fetch = mock(() => 
  Promise.resolve(new Response("mocked"))
);

Async Testing

Promise Testing

typescript
test("promise test", async () => {
  const promise = Promise.resolve(42);
  await expect(promise).resolves.toBe(42);
  
  const rejected = Promise.reject(new Error("Failed"));
  await expect(rejected).rejects.toThrow("Failed");
});

Timer Testing

typescript
import { test, expect, setSystemTime } from "bun:test";

test("timer test", () => {
  // Mock system time
  setSystemTime(new Date("2024-01-01T00:00:00Z"));
  
  expect(new Date().toISOString()).toBe("2024-01-01T00:00:00.000Z");
  
  // Restore real time
  setSystemTime();
});

Practical Test Examples

API Service Testing

typescript
// api.test.ts
import { describe, test, expect, beforeAll, afterAll } from "bun:test";

describe("API testing", () => {
  let server: ReturnType<typeof Bun.serve>;
  let baseUrl: string;
  
  beforeAll(() => {
    server = Bun.serve({
      port: 0, // Random port
      fetch(request) {
        const url = new URL(request.url);
        
        if (url.pathname === "/api/users") {
          return Response.json([{ id: 1, name: "张三" }]);
        }
        
        return new Response("Not Found", { status: 404 });
      },
    });
    
    baseUrl = `http://localhost:${server.port}`;
  });
  
  afterAll(() => {
    server.stop();
  });
  
  test("get user list", async () => {
    const response = await fetch(`${baseUrl}/api/users`);
    
    expect(response.ok).toBe(true);
    
    const users = await response.json();
    expect(users).toHaveLength(1);
    expect(users[0].name).toBe("张三");
  });
  
  test("404 response", async () => {
    const response = await fetch(`${baseUrl}/not-found`);
    expect(response.status).toBe(404);
  });
});

Database Testing

typescript
// db.test.ts
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { Database } from "bun:sqlite";

describe("database testing", () => {
  let db: Database;
  
  beforeEach(() => {
    db = new Database(":memory:");
    db.run(`
      CREATE TABLE users (
        id INTEGER PRIMARY KEY,
        name TEXT NOT NULL,
        email TEXT UNIQUE
      )
    `);
  });
  
  afterEach(() => {
    db.close();
  });
  
  test("insert user", () => {
    const stmt = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
    stmt.run("张三", "zhangsan@example.com");
    
    const user = db.query("SELECT * FROM users WHERE name = ?").get("张三");
    expect(user).toMatchObject({ name: "张三", email: "zhangsan@example.com" });
  });
  
  test("query users", () => {
    db.run("INSERT INTO users (name, email) VALUES ('张三', 'a@b.com')");
    db.run("INSERT INTO users (name, email) VALUES ('李四', 'c@d.com')");
    
    const users = db.query("SELECT * FROM users").all();
    expect(users).toHaveLength(2);
  });
});

Summary

This chapter covered:

  • ✅ Test file creation and execution
  • ✅ test, describe, and lifecycle hooks
  • ✅ Various assertion methods
  • ✅ Mock and Spy functions
  • ✅ Snapshot testing
  • ✅ Code coverage
  • ✅ Practical test examples

Next Steps

Continue reading Hot Reload to learn about Bun's development-time auto-reload feature.

Content is for learning and research only.