Lifecycle hooks overview
Bun test runner supports five lifecycle hooks: beforeAll (runs once before all tests), beforeEach (runs before each test), afterEach (runs after each test), afterAll (runs once after all tests), and onTestFinished (runs after a single test finishes, after all afterEach hooks).
onTestFinished hook not supported in concurrent tests
The onTestFinished hook is not supported in concurrent tests. Use test.serial instead for tests that require onTestFinished.
beforeEach and afterEach per-test setup and teardown
Use beforeEach to perform setup before each individual test runs, and afterEach to perform cleanup after each test completes. These hooks run for every test in their scope.
beforeAll and afterAll hook scope
The scope of beforeAll and afterAll hooks is determined by where they are defined. They can be scoped to a describe block if defined within it, scoped to an entire test file if defined at file level, or scoped to a global multi-file test run if defined in a separate setup file and loaded with --preload.
Global setup and teardown with --preload
To run hooks globally across all test files, define beforeAll and afterAll in a separate setup file, then run tests with --preload flag: bun test --preload ./setup.ts. To avoid typing --preload every time, add preload = ["./setup.ts"] to the [test] section of bunfig.toml.
All lifecycle hooks support async functions
beforeAll, beforeEach, afterEach, afterAll, and onTestFinished all support async functions. Tests wait for async beforeAll hooks to complete before running.
Nested hooks execution order
When using nested describe blocks, hooks execute in a specific order: File beforeAll, then outer describe beforeAll, then inner describe beforeAll, then outer beforeEach, then inner beforeEach, then the test runs, then inner afterEach, then outer afterEach, then inner afterAll, then outer afterAll, then file afterAll.
beforeAll error handling skips tests in scope
If a beforeAll hook throws an error, every test in its scope is skipped. The test suite will fail. To handle setup failures, catch the error, log it, and re-throw it to fail the suite.
Example: Per-test setup and teardown with beforeEach and afterEach
import { beforeEach, afterEach, test } from "bun:test";
beforeEach(() => {
console.log("running test.");
});
afterEach(() => {
console.log("done with test.");
});
test("example test", () => {
// This test will have beforeEach run before it
// and afterEach run after it
});
Example: Describe block scoped hooks
import { describe, beforeAll, afterAll, test } from "bun:test";
describe("test group", () => {
beforeAll(() => {
console.log("Setting up test group");
});
afterAll(() => {
console.log("Tearing down test group");
});
test("test 1", () => {
// test implementation
});
test("test 2", () => {
// test implementation
});
});
Example: File-level scoped hooks
import { describe, beforeAll, afterAll, test } from "bun:test";
beforeAll(() => {
console.log("Setting up test file");
});
afterAll(() => {
console.log("Tearing down test file");
});
describe("test group", () => {
test("test 1", () => {
// test implementation
});
});
Example: onTestFinished hook
import { test, onTestFinished } from "bun:test";
test("cleanup after test", () => {
onTestFinished(() => {
console.log("test finished");
});
});
Example: Global setup and teardown
In setup.ts:
import { beforeAll, afterAll } from "bun:test";
beforeAll(() => {
console.log("Global test setup");
});
afterAll(() => {
console.log("Global test teardown");
});
Then run: bun test --preload ./setup.ts
Example: Database setup with lifecycle hooks
import { beforeAll, afterAll, beforeEach, afterEach } from "bun:test";
import { createConnection, closeConnection, clearDatabase } from "./db";
let connection;
beforeAll(async () => {
connection = await createConnection({
host: "localhost",
database: "test_db",
});
});
afterAll(async () => {
await closeConnection(connection);
});
beforeEach(async () => {
await clearDatabase(connection);
});
Example: API server setup with lifecycle hooks
import { beforeAll, afterAll } from "bun:test";
import { startServer, stopServer } from "./server";
let server;
beforeAll(async () => {
server = await startServer({
port: 3001,
env: "test",
});
});
afterAll(async () => {
await stopServer(server);
});
Example: Mock setup with lifecycle hooks
import { beforeEach, afterEach } from "bun:test";
import { mock } from "bun:test";
beforeEach(() => {
mock.module("./api-client", () => ({
fetchUser: mock(() => Promise.resolve({ id: 1, name: "Test User" })),
createUser: mock(() => Promise.resolve({ id: 2 })),
}));
});
afterEach(() => {
mock.restore();
});
Example: Async lifecycle hooks
import { beforeAll, afterAll, test } from "bun:test";
beforeAll(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
console.log("Async setup complete");
});
afterAll(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
console.log("Async teardown complete");
});
test("async test", async () => {
await expect(Promise.resolve("test")).resolves.toBe("test");
});
Example: Nested hooks execution
import { describe, beforeAll, beforeEach, afterEach, afterAll, test } from "bun:test";
beforeAll(() => console.log("File beforeAll"));
afterAll(() => console.log("File afterAll"));
describe("outer describe", () => {
beforeAll(() => console.log("Outer beforeAll"));
beforeEach(() => console.log("Outer beforeEach"));
afterEach(() => console.log("Outer afterEach"));
afterAll(() => console.log("Outer afterAll"));
describe("inner describe", () => {
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");
});
});
});
// Execution order: File beforeAll, Outer beforeAll, Inner beforeAll, Outer beforeEach, Inner beforeEach, Test running, Inner afterEach, Outer afterEach, Inner afterAll, Outer afterAll, File afterAll
Example: beforeAll error handling
import { beforeAll, test, expect } from "bun:test";
beforeAll(async () => {
try {
await setupDatabase();
} catch (error) {
console.error("Database setup failed:", error);
throw error;
}
});
Best practice: Keep hooks simple and focused
Lifecycle hooks should be simple and focused on specific setup and teardown tasks. Avoid putting complex logic in hooks as it makes tests hard to debug. Good hooks do things like clearLocalStorage() or resetMocks(). Avoid doing complex operations like fetching data, processing it, and setting up multiple services in a single hook.
Best practice: Use appropriate hook scope
Use file-level beforeAll and afterAll for expensive shared resources like test servers. Use test-level beforeEach and afterEach for test-specific state like creating individual test users. This improves test performance and clarity.
Best practice: Clean up resources in afterEach and afterAll
Always clean up resources in afterEach and afterAll hooks. For each test, clear state like document.body.innerHTML and localStorage.clear(). For expensive resources, use afterAll to close database connections and stop servers.
Preload-level hooks with --parallel and --no-isolate
With --parallel --no-isolate, preload-level beforeAll/afterAll hooks still wrap every file, since a worker never knows which file is its last.
Test isolation requires cleanup with afterEach
Since tests run in the same process, ensure proper cleanup in afterEach hooks to clean up global state, delete environment variables, and restore mocked functions to prevent state leakage between tests.
Async tests with async/await
Test functions can be declared as async to test asynchronous code. Await promises directly within the test function.
Async tests with done callback
Alternatively, test functions can accept a 'done' parameter as a callback. The test must call done() to signal completion, or it will hang.
Timeout behavior kills child processes
When a test times out, Bun throws an uncatchable exception to force the test to stop and fail. Bun also kills any child processes spawned in the test (via Bun.spawn, Bun.spawnSync, or node:child_process) to prevent zombie processes.
Setup with beforeEach and teardown with afterEach
Use beforeEach() to run setup code before each test and afterEach() to run cleanup code after each test. These are imported from bun:test.