Lifecycle hooks
Learn how to use beforeAll, beforeEach, afterEach, and afterAll lifecycle hooks in Bun tests
The test runner supports the following lifecycle hooks. Use them to load test fixtures, mock data, and configure the test environment.
| Hook | Description |
|---|---|
beforeAll | Runs once before all tests. |
beforeEach | Runs before each test. |
afterEach | Runs after each test. |
afterAll | Runs once after all tests. |
onTestFinished | Runs after a single test finishes (after all afterEach). |
Per-Test Setup and Teardown#
Perform per-test setup and teardown logic with beforeEach and afterEach.
import { beforeEach, afterEach, test } from "bun:test";
beforeEach(() => {
console.log("running test.");
});
afterEach(() => {
console.log("done with test.");
});
// tests...
test("example test", () => {
// This test will have beforeEach run before it
// and afterEach run after it
});Per-Scope Setup and Teardown#
Perform per-scope setup and teardown logic with beforeAll and afterAll. Where you define the hook determines its scope.
Scoped to a Describe Block#
To scope the hooks to a particular describe block:
import { describe, beforeAll, afterAll, test } from "bun:test";
describe("test group", () => {
beforeAll(() => {
// setup for this describe block
console.log("Setting up test group");
});
afterAll(() => {
// teardown for this describe block
console.log("Tearing down test group");
});
test("test 1", () => {
// test implementation
});
test("test 2", () => {
// test implementation
});
});Scoped to a Test File#
To scope the hooks to an entire test file:
import { describe, beforeAll, afterAll, test } from "bun:test";
beforeAll(() => {
// setup for entire file
console.log("Setting up test file");
});
afterAll(() => {
// teardown for entire file
console.log("Tearing down test file");
});
describe("test group", () => {
test("test 1", () => {
// test implementation
});
});onTestFinished#
Use onTestFinished to run a callback after a single test completes. The callback runs after all afterEach hooks.
import { test, onTestFinished } from "bun:test";
test("cleanup after test", () => {
onTestFinished(() => {
// runs after all afterEach hooks
console.log("test finished");
});
});Not supported in concurrent tests; use test.serial instead.
Global Setup and Teardown#
To scope the hooks to an entire multi-file test run, define the hooks in a separate file.
import { beforeAll, afterAll } from "bun:test";
beforeAll(() => {
// global setup
console.log("Global test setup");
// Initialize database connections, start servers, etc.
});
afterAll(() => {
// global teardown
console.log("Global test teardown");
// Close database connections, stop servers, etc.
});Then use --preload to run the setup script before any test files.
bun test --preload ./setup.tsTo avoid typing --preload every time you run tests, add it to your bunfig.toml:
[test]
preload = ["./setup.ts"]Practical Examples#
Database Setup#
import { beforeAll, afterAll, beforeEach, afterEach } from "bun:test";
import { createConnection, closeConnection, clearDatabase } from "./db";
let connection;
beforeAll(async () => {
// Connect to test database
connection = await createConnection({
host: "localhost",
database: "test_db",
});
});
afterAll(async () => {
// Close database connection
await closeConnection(connection);
});
beforeEach(async () => {
// Start with clean database for each test
await clearDatabase(connection);
});API Server Setup#
import { beforeAll, afterAll } from "bun:test";
import { startServer, stopServer } from "./server";
let server;
beforeAll(async () => {
// Start test server
server = await startServer({
port: 3001,
env: "test",
});
});
afterAll(async () => {
// Stop test server
await stopServer(server);
});Mock Setup#
import { beforeEach, afterEach } from "bun:test";
import { mock } from "bun:test";
beforeEach(() => {
// Set up common mocks
mock.module("./api-client", () => ({
fetchUser: mock(() => Promise.resolve({ id: 1, name: "Test User" })),
createUser: mock(() => Promise.resolve({ id: 2 })),
}));
});
afterEach(() => {
// Clear all mocks after each test
mock.clearAllMocks();
});Async Lifecycle Hooks#
All lifecycle hooks support async functions:
import { beforeAll, afterAll, test, expect } from "bun:test";
beforeAll(async () => {
// Async setup
await new Promise(resolve => setTimeout(resolve, 100));
console.log("Async setup complete");
});
afterAll(async () => {
// Async teardown
await new Promise(resolve => setTimeout(resolve, 100));
console.log("Async teardown complete");
});
test("async test", async () => {
// Test will wait for beforeAll to complete
await expect(Promise.resolve("test")).resolves.toBe("test");
});Nested Hooks#
You can nest hooks. They run in the following order:
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");
});
});
});// Output order:
// File beforeAll
// Outer beforeAll
// Inner beforeAll
// Outer beforeEach
// Inner beforeEach
// Test running
// Inner afterEach
// Outer afterEach
// Inner afterAll
// Outer afterAll
// File afterAllError Handling#
If a beforeAll hook throws, the test runner skips every test in the hook's scope:
import { beforeAll, test } from "bun:test";
beforeAll(() => {
// If this throws, all tests in this scope will be skipped
throw new Error("Setup failed");
});
test("this test will be skipped", () => {
// This won't run because beforeAll failed
});To log a setup failure and still fail the suite:
import { beforeAll, test, expect } from "bun:test";
beforeAll(async () => {
try {
await setupDatabase();
} catch (error) {
console.error("Database setup failed:", error);
throw error; // Re-throw to fail the test suite
}
});Best Practices#
Keep Hooks Simple#
// Good: Simple, focused setup
beforeEach(() => {
clearLocalStorage();
resetMocks();
});
// Avoid: Complex logic in hooks
beforeEach(async () => {
// Too much complex logic makes tests hard to debug
const data = await fetchComplexData();
await processData(data);
await setupMultipleServices(data);
});Use Appropriate Scope#
// Good: File-level setup for shared resources
beforeAll(async () => {
await startTestServer();
});
// Good: Test-level setup for test-specific state
beforeEach(() => {
user = createTestUser();
});Clean Up Resources#
import { afterAll, afterEach } from "bun:test";
afterEach(() => {
// Clean up after each test
document.body.innerHTML = "";
localStorage.clear();
});
afterAll(async () => {
// Clean up expensive resources
await closeDatabase();
await stopServer();
});