PLAYWRIGHT UIINTERMEDIATE

Playwright Hooks Explained: beforeAll, beforeEach, afterEach & afterAll with Real Examples

Master Playwright hooks — beforeAll, beforeEach, afterEach, and afterAll. Learn execution order, scoping, hook vs fixture, best practices, and real test setup patterns for scalable Playwright frameworks.

iff Solution Academy July 23, 2026 14 min read Updated July 23, 2026
Playwright Playwright Hooks beforeEach afterEach beforeAll afterAll Test Setup Test Teardown Playwright Fixtures

Introduction

Every serious Playwright test suite needs setup and teardown logic — logging in a user, seeding a database, resetting state, or capturing artifacts on failure. Playwright gives you four hooks to control this lifecycle: beforeAll, beforeEach, afterEach, and afterAll.

Used correctly, hooks make your tests faster, cleaner, and more reliable. Used incorrectly, they cause hidden coupling, flaky failures, and slow suites. This guide covers every hook with real examples, execution order, scoping rules, and when to reach for a fixture instead.

What Are Playwright Hooks?

Hooks are special functions that run automatically at defined points around your tests. They come from @playwright/test and let you inject setup or cleanup code without repeating it in every test.

  • beforeAll — runs once before all tests in a file or describe block.
  • beforeEach — runs before every individual test.
  • afterEach — runs after every individual test.
  • afterAll — runs once after all tests in a file or describe block.

The Four Hook Types

tests/hooks-overview.spec.ts
import { test, expect } from '@playwright/test';

test.beforeAll(async () => {
  console.log('Runs ONCE before all tests');
});

test.beforeEach(async ({ page }) => {
  console.log('Runs before EACH test');
  await page.goto('https://example.com');
});

test.afterEach(async ({ page }, testInfo) => {
  console.log(`Runs after EACH test: ${testInfo.title} — ${testInfo.status}`);
});

test.afterAll(async () => {
  console.log('Runs ONCE after all tests');
});

test('first test', async ({ page }) => {
  await expect(page).toHaveTitle(/Example/);
});

test('second test', async ({ page }) => {
  await expect(page.locator('h1')).toBeVisible();
});

Execution Order

Playwright guarantees a strict order. For a file with two tests, the sequence is:

  1. beforeAll
  2. beforeEach → test 1 → afterEach
  3. beforeEach → test 2 → afterEach
  4. afterAll
Each Playwright worker runs its own copy of beforeAll and afterAll. If you shard tests across 4 workers, beforeAll runs 4 times — once per worker — not globally.

beforeAll in Practice

Use beforeAll for expensive, read-only setup that every test in the file shares — seeding data, generating tokens, or warming up an API client.

tests/auth-token.spec.ts
import { test, expect, request, APIRequestContext } from '@playwright/test';

let apiContext: APIRequestContext;
let authToken: string;

test.beforeAll(async () => {
  apiContext = await request.newContext({ baseURL: 'https://api.example.com' });
  const res = await apiContext.post('/login', {
    data: { email: 'qa@example.com', password: 'secret' },
  });
  authToken = (await res.json()).token;
});

test('fetch user profile', async () => {
  const res = await apiContext.get('/me', {
    headers: { Authorization: `Bearer ${authToken}` },
  });
  expect(res.status()).toBe(200);
});

test.afterAll(async () => {
  await apiContext.dispose();
});

beforeEach in Practice

Use beforeEach for setup that must be fresh for each test — navigating to a starting URL, resetting UI state, or logging in through a persisted storage state.

tests/dashboard.spec.ts
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});

test('shows welcome message', async ({ page }) => {
  await expect(page.getByText('Welcome back')).toBeVisible();
});

test('shows sidebar links', async ({ page }) => {
  await expect(page.getByRole('link', { name: 'Reports' })).toBeVisible();
});

afterEach in Practice

afterEach is perfect for per-test cleanup — clearing cookies, deleting created records, or capturing artifacts when a test fails.

tests/screenshot-on-failure.spec.ts
import { test } from '@playwright/test';

test.afterEach(async ({ page }, testInfo) => {
  if (testInfo.status !== testInfo.expectedStatus) {
    const screenshotPath = testInfo.outputPath(`failure-${testInfo.title}.png`);
    await page.screenshot({ path: screenshotPath, fullPage: true });
    await testInfo.attach('failure-screenshot', {
      path: screenshotPath,
      contentType: 'image/png',
    });
  }
});

afterAll in Practice

Use afterAll to release resources created in beforeAll — disposing API contexts, closing DB connections, or deleting seed data.

tests/db-cleanup.spec.ts
import { test } from '@playwright/test';
import { seedUsers, deleteUsers } from '../utils/db';

let createdIds: string[] = [];

test.beforeAll(async () => {
  createdIds = await seedUsers(5);
});

test.afterAll(async () => {
  await deleteUsers(createdIds);
});

Scoping with describe Blocks

Hooks respect the block they're declared in. A hook inside a describe block only runs for tests within that block — top-level hooks apply to the whole file.

tests/scoped-hooks.spec.ts
import { test } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  await page.goto('/');
});

test.describe('admin section', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/admin');
  });

  test('shows admin panel', async ({ page }) => {
    // Runs: top beforeEach -> describe beforeEach -> test
  });
});

test('home page loads', async ({ page }) => {
  // Runs: top beforeEach -> test (describe beforeEach is skipped)
});
When multiple beforeEach hooks apply, they run in declaration order: outer first, then inner. afterEach runs in reverse: inner first, then outer.

Hooks vs Fixtures

Hooks are convenient but global to a file — they share state through module-level variables and can't be reused across files. Fixtures solve the same problem in a composable, type-safe way.

tests/authenticated.spec.ts
// Hook approach - state in module scope
let token: string;
test.beforeAll(async () => { token = await login(); });

// Fixture approach - reusable across files
export const test = base.extend<{ token: string }>({
  token: async ({}, use) => {
    const t = await login();
    await use(t);
  },
});
  • Use hooks for per-file setup that won't be reused elsewhere.
  • Use fixtures when the same setup is needed across multiple spec files.
  • Fixtures give you dependency injection, automatic cleanup, and per-test isolation without shared module state.

Best Practices

  • Keep hooks small — they run for every test, so slow hooks slow the whole suite.
  • Never assert business logic inside a hook. Assertions belong in tests so failures point at the right place.
  • Prefer beforeEach over beforeAll for anything that mutates state — shared mutable state causes flaky, order-dependent tests.
  • Always pair beforeAll with afterAll for resources like API contexts, DB connections, or spawned processes.
  • Use testInfo.status inside afterEach to capture artifacts only on failure — avoids screenshot noise on green runs.
  • Move reusable setup into a fixture as soon as more than one spec file needs it.

Common Mistakes

  • Sharing mutable state in beforeAll and relying on test execution order — Playwright can parallelise tests and reorder them.
  • Doing heavy UI login in beforeEach for every test instead of using storageState — kills suite speed.
  • Forgetting that beforeAll runs per worker, not globally. Use a globalSetup file for truly one-time setup.
  • Throwing inside afterAll — errors there mark the whole file as failed even if every test passed.
  • Mixing hooks and fixtures for the same concern, which duplicates work and hides ownership.

Hooks and Parallel Execution

Hooks behave differently once tests run in parallel, and misunderstanding that behaviour is a frequent source of confusing failures in large suites.

Playwright's default unit of parallelism is the test file, and each worker process is fully isolated with its own module registry and browser instance. That means beforeAll does not run once for the whole suite — it runs once per file, per worker. A file that is split across shards, or retried in a fresh worker, will execute its beforeAll again. Any setup you place there must therefore be safe to run multiple times.

When you enable fullyParallel or mark a describe block with the parallel mode, tests within the same file are distributed across workers as well. In that configuration, beforeAll runs in every worker that picks up at least one test from the file, and state assigned to a module-level variable in one worker is invisible to the others. Data shared through a variable will silently be undefined in the workers that did not run the setup.

  • Assume beforeAll can run several times; make it idempotent rather than assuming a single execution.
  • Never share mutable state between tests through module-level variables in parallel mode.
  • Give each worker unique test data by seeding names and emails with the worker index.
  • Use serial mode only when tests genuinely depend on order, and accept that it disables parallel gains for that file.
  • Put truly global, one-time setup in globalSetup, not in beforeAll.

Handling Failures Inside Hooks

A failure inside a hook is more disruptive than a failure inside a test, because it usually invalidates every test that depended on that setup.

If beforeAll throws, Playwright skips all tests in that scope and reports the hook error. If beforeEach throws, the individual test is marked as failed and its afterEach still runs, which is important because cleanup must not be skipped just because setup failed. A failure in afterEach or afterAll marks the enclosing scope as failed even when every test passed, which is exactly the behaviour you want when teardown leaves the environment dirty.

Write teardown defensively. Wrap cleanup in try/catch when a partially completed setup may leave resources in an unknown state, and check that a resource exists before attempting to delete it. A teardown that throws a null-reference error hides the original failure and makes triage far harder than it needs to be.

Keep hooks short. Long, multi-step hooks that log in, seed data, configure feature flags, and navigate are difficult to debug because the report shows a single failing hook rather than the step that broke. Extracting each concern into a named helper function gives you readable stack traces and lets you reuse the same setup across files.

Moving from Hooks to Fixtures

Hooks are the right tool for simple, file-local setup. Once setup starts being copied between files, or a hook needs to provide a value to the test, fixtures are the better abstraction — and moving to them is usually straightforward.

Start by identifying hooks that assign to a shared variable, since those translate almost mechanically into a fixture that yields the same object. A beforeEach that constructs a logged-in page becomes an authenticated page fixture, and every test that used the shared variable now receives it as a parameter, which removes the ordering assumption entirely.

Fixtures also give you automatic cleanup: whatever follows the yield in a fixture runs even if the test fails, so paired beforeEach and afterEach logic collapses into a single, colocated definition. Worker-scoped fixtures replace beforeAll for expensive resources such as a database connection or an authentication token, and Playwright guarantees one instance per worker.

A pragmatic end state for most enterprise frameworks is fixtures for anything shared or reusable, and hooks reserved for small, obviously local details such as navigating to the page under test at the start of a describe block. Both mechanisms coexist happily, so migration can be gradual.

Frequently Asked Questions

In what order do nested hooks run?

Outer beforeAll, then outer beforeEach, then inner beforeEach, then the test, then inner afterEach, outer afterEach, and finally outer afterAll. Before hooks run outside-in and after hooks run inside-out.

Can I use page inside beforeAll?

Not the built-in page fixture, because it is test-scoped and created fresh for each test. Create a browser context and page manually inside beforeAll if you need one there, and close it in afterAll.

Does afterEach run when a test fails?

Yes. Teardown hooks run regardless of the test result, which is why cleanup belongs there rather than at the end of the test body.

Should I put login in beforeEach?

Only for a handful of tests. For a real suite, authenticate once and reuse the stored session state, which is dramatically faster than logging in through the UI before every test.

Summary

Hooks are the backbone of Playwright test lifecycle management. beforeAll and afterAll handle expensive one-time setup and cleanup per worker. beforeEach and afterEach keep every test isolated and capture artifacts on failure.

As your framework grows, promote repeated hook logic into fixtures — you get reusability, type safety, and automatic cleanup for free. Combined with globalSetup for one-time initialization, hooks and fixtures let you scale from a handful of tests to enterprise-grade suites without losing reliability.

Rule of thumb: if two spec files copy the same beforeEach, it's time for a fixture. If setup must happen exactly once for the whole run, use globalSetup — not beforeAll.

Playwright Framework Series

View all →
  1. 1Getting Started with Playwright: Installation, Setup, and Your First Test
  2. 2Playwright Locators: The Complete Guide with Real-World Examples
  3. 3Playwright Actions: Complete Guide to Click, Fill, Hover, Keyboard, Mouse & File Upload
  4. 4Playwright Assertions Complete Guide: Web-First Assertions, Auto-Retry & Custom Matchers
  5. 5Playwright Auto Waiting: The Complete Guide with Real-World Examples
  6. 6Playwright Fixtures: The Complete Guide with Real-World Examples
  7. 7Playwright Browser Context & Multiple Tabs: The Complete Guide with Real-World Examples
  8. 8Playwright Authentication & Session Management: Complete Guide with Enterprise Examples
  9. 9Playwright Network Interception & API Mocking: Complete Guide with Real-World Examples
  10. 10Playwright Page Object Model (POM): The Complete Guide with Enterprise Examples
  11. 11Page Object Model with Playwright: A Practical Guide
  12. 12How to Build a Robust Professional Playwright Framework from Scratch (Step-by-Step)
  13. 13Building an Enterprise Playwright Framework from Scratch

API Testing Series

View all →
  1. 1Part 1 : Playwright API Testing Tutorial: Build Enterprise-Level API Automation Framework
  2. 2Part 2: Playwright API Testing Tutorial: Setting Up Project & Sending Your First API Request
  3. 3Part 3: Mastering CRUD Operations in Playwright API Testing
  4. 4Part 4: Authentication in Playwright API Testing (Bearer Token, JWT, API Key & Reusable Fixtures)
  5. 5Part 5: Building an Enterprise-Level Playwright API Automation Framework
  6. 6Part 6: API Models, Schema Validation & Test Data Management in Playwright
  7. 7Part 7: Custom Fixtures, Hooks, Logging & Parallel Execution in Playwright API Testing
  8. 8Part 8: Building a Production-Ready Playwright API Framework (Environment Management, CI/CD, Reporting & Best Practices)
  9. 9Part 9: Advanced Playwright API Testing Techniques for Enterprise Automation
  10. 10Part 10: Building a Complete Enterprise Playwright API Automation Framework (Final Part)
  11. 11Playwright Backend Testing Tutorial – REST API, GraphQL, WebSocket, Kafka, Database & Microservices

Recommended Next Articles