# Lifecycle hooks (/docs/test/lifecycle)

<!-- agent-signals: reading_time_min: 6 · est_tokens: 2256 · updated: 2026-07-28 -->
Related: [Writing tests](/docs/test/writing-tests.md), [Test configuration](/docs/test/configuration.md), [Runtime behavior](/docs/test/runtime-behavior.md), [Finding tests](/docs/test/discovery.md), [Mocks](/docs/test/mocks.md), [Snapshots](/docs/test/snapshots.md)

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 [#per-test-setup-and-teardown]

Perform per-test setup and teardown logic with `beforeEach` and `afterEach`.

```ts title="test.ts" icon="/icons/typescript.svg"
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 [#per-scope-setup-and-teardown]

Perform per-scope setup and teardown logic with `beforeAll` and `afterAll`. The scope is determined by where the hook is defined.

### Scoped to a Describe Block [#scoped-to-a-describe-block]

To scope the hooks to a particular describe block:

```ts title="test.ts" icon="/icons/typescript.svg"
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 [#scoped-to-a-test-file]

To scope the hooks to an entire test file:

```ts title="test.ts" icon="/icons/typescript.svg"
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` [#ontestfinished]

Use `onTestFinished` to run a callback after a single test completes. It runs after all `afterEach` hooks.

```ts title="test.ts" icon="/icons/typescript.svg"
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 [#global-setup-and-teardown]

To scope the hooks to an entire multi-file test run, define the hooks in a separate file.

```ts title="setup.ts" icon="/icons/typescript.svg"
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.

```bash terminal icon="terminal"
bun test --preload ./setup.ts
```

To avoid typing `--preload` every time you run tests, add it to your `bunfig.toml`:

```toml title="bunfig.toml" icon="settings"
[test]
preload = ["./setup.ts"]
```

## Practical Examples [#practical-examples]

### Database Setup [#database-setup]

```ts title="database-setup.ts" icon="/icons/typescript.svg"
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 [#api-server-setup]

```ts title="server-setup.ts" icon="/icons/typescript.svg"
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 [#mock-setup]

```ts title="mock-setup.ts" icon="/icons/typescript.svg"
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();
});
```

## Async Lifecycle Hooks [#async-lifecycle-hooks]

All lifecycle hooks support async functions:

```ts title="test.ts" icon="/icons/typescript.svg"
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");
});
```

## Nested Hooks [#nested-hooks]

Hooks can be nested. They run in the following order:

```ts title="test.ts" icon="/icons/typescript.svg"
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");
    });
  });
});
```

```txt
// Output order:
// File beforeAll
// Outer beforeAll
// Inner beforeAll
// Outer beforeEach
// Inner beforeEach
// Test running
// Inner afterEach
// Outer afterEach
// Inner afterAll
// Outer afterAll
// File afterAll
```

## Error Handling [#error-handling]

If a `beforeAll` hook throws, every test in its scope is skipped:

```ts title="test.ts" icon="/icons/typescript.svg"
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:

```ts title="test.ts" icon="/icons/typescript.svg"
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 [#best-practices]

### Keep Hooks Simple [#keep-hooks-simple]

```ts title="test.ts" icon="/icons/typescript.svg"
// 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 [#use-appropriate-scope]

```ts title="test.ts" icon="/icons/typescript.svg"
// 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 [#clean-up-resources]

```ts title="test.ts" icon="/icons/typescript.svg"
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();
});
```
