Skip to main content
Bun’s test runner supports lifecycle hooks for loading fixtures, managing shared resources, and configuring the test environment.
HookDescription
beforeAllRuns once before all tests in the current scope.
beforeEachRuns before each test in the current scope.
afterEachRuns after each test in the current scope.
afterAllRuns once after all tests in the current scope.
onTestFinishedRuns after a single test completes (after all afterEach).

Per-test setup and teardown

Use beforeEach and afterEach for setup and cleanup that should happen around every test:
import { beforeEach, afterEach, test, expect } from "bun:test";

let testUser: User;

beforeEach(() => {
  testUser = createTestUser();
});

afterEach(() => {
  cleanupTestUser(testUser);
});

test("updates user profile", () => {
  testUser.name = "Updated";
  expect(testUser.name).toBe("Updated");
});

Per-scope setup and teardown

Use beforeAll and afterAll for setup and cleanup that should happen once for a group of tests. The scope is determined by where the hook is defined.

Scoped to a describe block

import { describe, beforeAll, afterAll, test } from "bun:test";

describe("UserService", () => {
  let db: Database;

  beforeAll(async () => {
    db = await connectTestDatabase();
  });

  afterAll(async () => {
    await db.close();
  });

  test("creates a user", async () => {
    const user = await db.createUser({ name: "Alice" });
    expect(user.id).toBeDefined();
  });

  test("finds a user", async () => {
    const user = await db.findUser("Alice");
    expect(user).not.toBeNull();
  });
});

Scoped to a test file

Hooks defined at the top level of a file apply to all tests in that file:
import { beforeAll, afterAll, test } from "bun:test";

let server: Server;

beforeAll(async () => {
  server = await startTestServer({ port: 0 });
});

afterAll(async () => {
  await server.stop();
});

test("GET /health returns 200", async () => {
  const res = await fetch(`http://localhost:${server.port}/health`);
  expect(res.status).toBe(200);
});

Global setup and teardown

To run hooks before any test file executes, define them in a preload file:
setup.ts
import { beforeAll, afterAll } from "bun:test";

beforeAll(async () => {
  await startTestInfrastructure();
});

afterAll(async () => {
  await stopTestInfrastructure();
});
Register the preload file in bunfig.toml:
bunfig.toml
[test]
preload = ["./setup.ts"]
Or pass it on the command line:
bun test --preload ./setup.ts

Async hooks

All hooks support async functions. Bun awaits each hook before proceeding.
import { beforeAll, afterAll, test, expect } from "bun:test";

beforeAll(async () => {
  await seedDatabase();
});

afterAll(async () => {
  await clearDatabase();
});

test("reads seeded data", async () => {
  const rows = await db.query("SELECT * FROM users");
  expect(rows.length).toBeGreaterThan(0);
});

onTestFinished

Use onTestFinished to register a callback that runs after a specific test completes, after all afterEach hooks:
import { test, onTestFinished } from "bun:test";

test("with per-test cleanup", () => {
  const tempFile = createTempFile();

  onTestFinished(() => {
    deleteTempFile(tempFile);
  });

  // ... test body
});
onTestFinished is not supported inside concurrent tests. Use test.serial for tests that need per-test cleanup via this hook.

Execution order

When hooks are nested inside describe blocks, they run in this order:
import { describe, beforeAll, beforeEach, afterEach, afterAll, test } from "bun:test";

beforeAll(() => console.log("File beforeAll"));
afterAll(() => console.log("File afterAll"));

describe("outer", () => {
  beforeAll(() => console.log("Outer beforeAll"));
  beforeEach(() => console.log("Outer beforeEach"));
  afterEach(() => console.log("Outer afterEach"));
  afterAll(() => console.log("Outer afterAll"));

  describe("inner", () => {
    beforeAll(() => console.log("Inner beforeAll"));
    beforeEach(() => console.log("Inner beforeEach"));
    afterEach(() => console.log("Inner afterEach"));
    afterAll(() => console.log("Inner afterAll"));

    test("nested test", () => {
      console.log("Test running");
    });
  });
});
Output:
File beforeAll
Outer beforeAll
Inner beforeAll
Outer beforeEach
Inner beforeEach
Test running
Inner afterEach
Outer afterEach
Inner afterAll
Outer afterAll
File afterAll

Parameterized tests

test.each

Run the same test function with multiple inputs:
import { test, expect } from "bun:test";

test.each([
  [1, 1, 2],
  [2, 3, 5],
  [10, 20, 30],
])("adds %i + %i = %i", (a, b, expected) => {
  expect(a + b).toBe(expected);
});
Pass objects to use named fields in the title:
test.each([
  { input: "hello", expected: 5 },
  { input: "world!", expected: 6 },
])("$input has length $expected", ({ input, expected }) => {
  expect(input.length).toBe(expected);
});

describe.each

Create a parameterized suite — every test inside runs once per data set:
import { describe, test, expect } from "bun:test";

describe.each([
  { role: "admin", canDelete: true },
  { role: "viewer", canDelete: false },
])("$role user", ({ role, canDelete }) => {
  test("can read resources", () => {
    expect(canRead(role)).toBe(true);
  });

  test(`can${canDelete ? "" : "not"} delete resources`, () => {
    expect(canDeleteResource(role)).toBe(canDelete);
  });
});

Error handling

If a hook throws, all tests in its scope are skipped:
beforeAll(async () => {
  try {
    await connectDatabase();
  } catch (error) {
    console.error("Database setup failed:", error);
    throw error; // causes all tests in scope to be skipped
  }
});

Best practices

Use beforeAll/afterAll for expensive shared resources (servers, database connections) and beforeEach/afterEach for per-test state (mock data, DOM resets).
// Expensive: start once
beforeAll(async () => { server = await startServer(); });
afterAll(async () => { await server.stop(); });

// Cheap: reset per test
beforeEach(() => { db.seed(testData); });
afterEach(() => { db.clear(); });
Failing to clean up can cause test pollution and flakiness:
afterEach(() => {
  document.body.innerHTML = "";
  localStorage.clear();
  jest.resetAllMocks();
});

afterAll(async () => {
  await closeDatabase();
  await stopServer();
});
Move complex setup logic into helper functions to keep hooks readable and testable:
beforeEach(async () => {
  await seedTestDatabase();  // encapsulates complexity
});

Build docs developers (and LLMs) love