Lifecycle hooks in Bun
Bun supports four lifecycle hooks: beforeAll (runs once before all tests), beforeEach (runs before each test), afterEach (runs after each test), and afterAll (runs once after all tests). Define hooks inside test files or in a separate file preloaded with the `--preload` flag.
Preload scripts before running tests
Use the `--preload` flag to load scripts before running tests: `bun test --preload ./setup.ts`. Preloaded files can contain lifecycle hooks or other setup logic shared across all test files.
Test isolation requires proper cleanup
Since tests run in the same process, ensure proper cleanup in lifecycle hooks. Use afterEach to clean up global state, delete environment variables, and restore mocked functions with jest.restoreAllMocks().
beforeAll lifecycle hook runs once before all tests
beforeAll is a lifecycle hook that runs once before all tests in its scope. It is used to perform setup logic that should execute only once, such as initializing shared resources.
beforeEach lifecycle hook runs before each test
beforeEach is a lifecycle hook that runs before each individual test in its scope. It is used for per-test setup logic, such as resetting state or clearing mocks before each test runs.
afterEach lifecycle hook runs after each test
afterEach is a lifecycle hook that runs after each individual test completes in its scope. It is used for per-test teardown logic, such as cleaning up resources or resetting state after each test.
afterAll lifecycle hook runs once after all tests
afterAll is a lifecycle hook that runs once after all tests in its scope have completed. It is used to perform final teardown logic, such as closing database connections or stopping servers.
onTestFinished runs after a test finishes, after all afterEach hooks
onTestFinished is a lifecycle hook that runs after a single test completes, after all afterEach hooks have run. It is not supported in concurrent tests; use test.serial instead.
Difference between beforeAll and beforeEach
beforeAll runs once before all tests in its scope, making it suitable for expensive setup operations. beforeEach runs before each individual test, making it suitable for per-test setup that needs to be repeated.
Scope hooks to a describe block by defining them inside the block
To scope lifecycle hooks to a particular describe block, define the hooks inside that describe block. The hooks will run only for tests within that describe block.
Scope hooks to entire test file by defining them at file level
To scope lifecycle hooks to an entire test file, define beforeAll and afterAll at the top level of the file, outside any describe blocks. These hooks will run once for all tests in the file.
Global setup and teardown using preload
To scope hooks to an entire multi-file test run, define beforeAll and afterAll in a separate setup file, then use the --preload flag to run the setup script before any test files. Add preload = ["./setup.ts"] to the [test] section of bunfig.toml to avoid typing --preload every time.
All lifecycle hooks support async functions
beforeAll, beforeEach, afterEach, afterAll, and onTestFinished all support async functions. Tests will wait for async setup to complete before running, and async teardown to complete before finishing.
Nested hooks execution order
When hooks are nested in multiple describe blocks, they execute in this order: File beforeAll → Outer beforeAll → Inner beforeAll → Outer beforeEach → Inner beforeEach → Test → Inner afterEach → Outer afterEach → Inner afterAll → Outer afterAll → File afterAll.
If beforeAll hook throws, all tests in its scope are skipped
When a beforeAll hook throws an error, the test runner skips every test in that hook's scope. To log a setup failure and still fail the suite, catch the error, log it, and re-throw it.
Example beforeEach and afterEach per-test setup
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 onTestFinished hook
import { test, onTestFinished } from "bun:test";
test("cleanup after test", () => {
onTestFinished(() => {
// runs after all afterEach hooks
console.log("test finished");
});
});
Example global setup and teardown with preload
Create setup.ts with:
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
Or add to bunfig.toml:
[test]
preload = ["./setup.ts"]
Example mock setup in beforeEach and afterEach
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.clearAllMocks();
});
onTestFinished is not supported in concurrent tests
onTestFinished cannot be used in concurrent tests. Use test.serial instead if you need sequential test execution with onTestFinished.
Preload-level beforeAll/afterAll hooks with --parallel --no-isolate
When using --parallel --no-isolate, preload-level beforeAll/afterAll hooks still wrap every file, since a worker never knows which file is its last.