Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/zhcndoc/bun/llms.txt

Use this file to discover all available pages before exploring further.

Bun’s test runner has specific runtime behavior that affects how tests are executed, how state is managed, and how resources are handled.

Test isolation

File-level isolation

Each test file runs in its own module scope:
// test1.test.ts
let counter = 0;

test("increment", () => {
  counter++;
  expect(counter).toBe(1);
});

// test2.test.ts
let counter = 0; // Different counter, separate module

test("increment", () => {
  counter++;
  expect(counter).toBe(1); // Also 1
});
Tests in different files don’t share global state.

Test-level isolation

Tests within the same file share the module scope:
import { test, expect } from "bun:test";

let counter = 0;

test("first test", () => {
  counter++;
  expect(counter).toBe(1);
});

test("second test", () => {
  counter++; // counter is now 2
  expect(counter).toBe(2);
});
Use beforeEach to reset state:
import { test, beforeEach, expect } from "bun:test";

let counter: number;

beforeEach(() => {
  counter = 0;
});

test("first test", () => {
  counter++;
  expect(counter).toBe(1);
});

test("second test", () => {
  counter++;
  expect(counter).toBe(1); // Reset to 0 by beforeEach
});

Execution model

Sequential execution

By default, tests in a file run sequentially:
test("test 1", () => console.log("1"));
test("test 2", () => console.log("2"));
test("test 3", () => console.log("3"));
// Output: 1, 2, 3

Concurrent execution

Mark tests as concurrent:
import { test } from "bun:test";

test.concurrent("test 1", async () => {
  await delay(100);
  console.log("1");
});

test.concurrent("test 2", async () => {
  await delay(50);
  console.log("2");
});

test.concurrent("test 3", async () => {
  await delay(10);
  console.log("3");
});
// Output: 3, 2, 1 (fastest first)

File-level concurrency

Run multiple test files in parallel:
$ bun test --concurrent
Or configure in bunfig.toml:
[test]
concurrent = true

Global state

Process environment

Environment variables are shared across all tests:
import { test, expect } from "bun:test";

process.env.TEST_VAR = "initial";

test("test 1", () => {
  process.env.TEST_VAR = "modified";
});

test("test 2", () => {
  expect(process.env.TEST_VAR).toBe("modified"); // Sees change from test 1
});
Reset in beforeEach:
import { test, beforeEach } from "bun:test";

const originalEnv = { ...process.env };

beforeEach(() => {
  process.env = { ...originalEnv };
});

Global objects

Changes to global objects persist:
import { test, expect } from "bun:test";

(globalThis as any).myGlobal = "initial";

test("test 1", () => {
  (globalThis as any).myGlobal = "modified";
});

test("test 2", () => {
  expect((globalThis as any).myGlobal).toBe("modified");
});

Module caching

Modules are cached per test file:
// utils.ts
export const timestamp = Date.now();

// test1.test.ts
import { timestamp } from "./utils";
test("test", () => {
  console.log(timestamp); // Always the same value
});
To clear cache, use dynamic import:
import { test } from "bun:test";

test("test", async () => {
  const { timestamp } = await import("./utils.ts");
  console.log(timestamp); // Fresh import
});

Resource management

Automatic cleanup with using

Bun supports JavaScript’s using keyword:
import { test } from "bun:test";

test("resource cleanup", () => {
  using server = createTestServer();
  // server.stop() called automatically
});

Manual cleanup

Use afterEach or afterAll for cleanup:
import { test, afterEach } from "bun:test";

const servers: Server[] = [];

afterEach(() => {
  servers.forEach(s => s.stop());
  servers.length = 0;
});

test("test 1", () => {
  const server = createServer();
  servers.push(server);
});

Error handling

Unhandled promise rejections

Bun catches unhandled rejections and fails the test:
import { test, expect } from "bun:test";

test("unhandled rejection", () => {
  Promise.reject(new Error("Oops")); // Test fails
});

Uncaught exceptions

Uncaught exceptions fail the test:
import { test } from "bun:test";

test("uncaught exception", () => {
  setTimeout(() => {
    throw new Error("Oops"); // Test fails
  }, 0);
});

Async test completion

Async tests must complete:
import { test, expect } from "bun:test";

test("async test", async () => {
  const result = await asyncOperation();
  expect(result).toBe(42);
  // Test completes when promise resolves
});

Memory management

Garbage collection

Bun runs garbage collection between test files:
import { test } from "bun:test";

test("creates large array", () => {
  const large = new Array(1_000_000).fill(0);
  // Memory freed after test file completes
});

Memory leaks

Avoid keeping references in global scope:
// ❌ Bad: Accumulates memory
const results: any[] = [];

test("test 1", () => {
  results.push(generateLargeData());
});

test("test 2", () => {
  results.push(generateLargeData());
});

// ✅ Good: Cleans up
let results: any[] = [];

afterEach(() => {
  results = [];
});

Performance characteristics

Test startup time

First test in a file includes:
  • Module loading
  • Dependency resolution
  • Global setup
import { test } from "bun:test";

test("first test", () => {
  // Includes startup overhead
});

test("second test", () => {
  // No startup overhead
});

Optimization tips

  1. Minimize global imports:
    // ✅ Good: Import only what you need
    import { test, expect } from "bun:test";
    
    // ❌ Bad: Imports entire library
    import * as everything from "bun:test";
    
  2. Use lazy imports for heavy dependencies:
    test("heavy import", async () => {
      const { heavy } = await import("./heavy-lib");
    });
    
  3. Share expensive setup with beforeAll:
    let db;
    beforeAll(async () => {
      db = await connectToDatabase();
    });
    

Timeout behavior

Default timeout

Tests timeout after 5 seconds by default:
import { test } from "bun:test";

test("slow test", async () => {
  await delay(10000); // Fails: timeout
});

Custom timeout

Set per-test timeout:
test("slow test", async () => {
  await delay(10000);
}, 15000); // 15 second timeout

Infinite operations

Ensure tests complete:
// ❌ Bad: Never completes
test("infinite loop", () => {
  while (true) {}
});

// ✅ Good: Completes
test("finite loop", () => {
  let count = 0;
  while (count < 100) {
    count++;
  }
});

Exit behavior

Normal exit

Bun exits with code 0 if all tests pass:
$ bun test
# Exit code: 0

Failure exit

Bun exits with code 1 if any test fails:
$ bun test
# Exit code: 1

Hanging processes

Bun waits for async operations to complete:
import { test } from "bun:test";

test("hanging test", () => {
  setInterval(() => {}, 1000); // Prevents exit
});
Clean up resources:
import { test, afterAll } from "bun:test";

const intervals: Timer[] = [];

afterAll(() => {
  intervals.forEach(clearInterval);
});

test("clean test", () => {
  const interval = setInterval(() => {}, 1000);
  intervals.push(interval);
});

Build docs developers (and LLMs) love