skills/bun-test-lifecycle/SKILL.md
Learn how to use beforeAll, beforeEach, afterEach, and afterAll lifecycle hooks in Bun tests
npx skillsauth add jarle/bun-skills Bun Lifecycle hooksInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Learn how to use beforeAll, beforeEach, afterEach, and afterAll lifecycle hooks in Bun tests
The test runner supports the following lifecycle hooks. This is useful for loading test fixtures, mocking data, and configuring 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). |
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
});
Perform per-scope setup and teardown logic with beforeAll and afterAll. The scope is determined by where the hook is defined.
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
});
});
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
});
});
onTestFinishedUse onTestFinished to run a callback after a single test completes. It 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.
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.ts
To avoid typing --preload every time you run tests, it can be added to your bunfig.toml:
[test]
preload = ["./setup.ts"]
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);
});
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);
});
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.restore();
});
All lifecycle hooks support async functions:
import { beforeAll, afterAll, test } 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");
});
Hooks can be nested and will run in the appropriate 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 afterAll
If a lifecycle hook throws an error, it will affect test execution:
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
});
For better error handling:
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
}
});
// 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);
});
// Good: File-level setup for shared resources
beforeAll(async () => {
await startTestServer();
});
// Good: Test-level setup for test-specific state
beforeEach(() => {
user = createTestUser();
});
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();
});
development
Using TypeScript with Bun, including type definitions and compiler options
development
Learn how to write tests using Bun's Jest-compatible API with support for async tests, timeouts, and various test modifiers
testing
Learn how to use snapshot testing in Bun to save and compare output between test runs
testing
Learn about Bun test's runtime integration, environment variables, timeouts, and error handling