Playwright Test Structure and Hooks

This chapter introduces how Playwright tests are organized, including test grouping, execution control, and the usage of lifecycle hooks.


test.describe() Test Grouping

test.describe()Used to organize related tests together into logical groups.

Example

// File path: tests/describe-demo.spec.ts
import { test, expect } from '@playwright/test';

// First-level grouping
test.describe('EXAMPLE homepage test', () => {

  test('Page title is correct', async ({ page }) => {
    await page.goto('https://www.example.com/');
    await expect(page).toHaveTitle(/EXAMPLE/);
  });

  test('Navigation bar exists', async ({ page }) => {
    await page.goto('https://www.example.com/');
    // Assert the navigation element exists
    await expect(page.locator('nav')).toBeVisible();
  });
});

// Multiple describe blocks can be defined
test.describe('Search functionality test', () => {

  test('Search box is visible', async ({ page }) => {
    await page.goto('https://www.example.com/');
    await expect(
      page.getByPlaceholder('Search')
    ).toBeVisible();
  });
});

The test report will bedescribedisplayed in groups, making it easy to view results.


Nested describe

test.describe()They can be nested to form a tree structure:

Example

test.describe('User module', () => {

  test.describe('Login functionality', () => {
    test('Login with correct account and password', async ({ page }) => { /* ... */ });
    test('Login fails with incorrect password', async ({ page }) => { /* ... */ });
  });

  test.describe('Registration functionality', () => {
    test('Register with correct information', async ({ page }) => { /* ... */ });
    test('Registration fails with an existing email', async ({ page }) => { /* ... */ });
  });
});

test.skip() Skip Tests

test.skip()Used to skip a test; skipped tests will not execute.

Example

// Skip directly (often used to temporarily disable failing feature tests)
test.skip('Feature test not yet completed', async ({ page }) => {
  // This test will not execute
});

// Conditional skip (determined at runtime)
test('Skip under specific conditions', async ({ page, browserName }) => {
  // Skip in Firefox
  test.skip(browserName === 'firefox', 'Firefox does not support this feature yet');

  // Test logic only runs in non-Firefox browsers
  await page.goto('https://www.example.com/');
});

test.fail() Mark Expected Failure

test.fail()Used to mark a test that is known to fail.

Example

// Marked as expected failure (will not be reported as a test failure)
test.fail('Known bug: search functionality is not yet implemented', async ({ page }) => {
  await page.goto('https://www.example.com/');
  // This assertion is expected to fail
  await expect(page.locator('.search-results')).toBeVisible();
});

// If the test unexpectedly passes, it will be reported as a failure
// This reminds you: "The bug is fixed, you can remove the test.fail marker now"

test.fail()Its purpose is reverse marking: test failure = as expected, test pass = abnormal (marker may need to be removed).


test.only() Run Only the Current Test

test.only()Used to run only specific tests during development and debugging, ignoring other tests in the file.

Example

// During development and debugging, only run the current test
test.only('I am debugging this test', async ({ page }) => {
  // Only this test will run
  await page.goto('https://www.example.com/');
});

test('This test will be ignored', async ({ page }) => {
  // Because of test.only() above, this will not run
});

test.only()is a debugging tool and should not be committed to the code repository. If youplaywright.config.tsset ... inforbidOnly: true, CI will detectonlyand cause the build to fail.


test.fixme() Mark as Needs Fixing

test.fixme()andtest.skip()Similar, but semantically it means "this test needs to be fixed."

Example

// Marked as needing fixing (will not run)
test.fixme('Login flow needs to be updated due to API changes', async ({ page }) => {
  // This test will not run for now
});

test.slow() Mark Slow Tests

test.slow()Increases the test timeout by 3 times.

Example

// Marked as a slow test, timeout is tripled
test.slow('Large data volume test', async ({ page }) => {
  // The default timeout for this test will be 90 seconds (30s × 3)
  // Suitable for scenarios that require heavy data loading or complex computation
});

Test Hooks

Hook functions let you perform setup and cleanup work at different stages of testing.

test.beforeEach() and test.afterEach()

beforeEachExecutes before each test,afterEachand executes after each test.

Example

// File path: tests/hooks-demo.spec.ts
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  // Navigate to the same page before each test
  await page.goto('https://www.example.com/');
});

test.afterEach(async ({ page }) => {
  // Perform cleanup after each test (e.g., remove created data)
  // Playwright automatically cleans up browser contexts,
  // The cleanup here mainly refers to server-side state
  console.log('Test finished, starting cleanup...');
});

test('First test', async ({ page }) => {
  // page is already on the EXAMPLE homepage
  await expect(page).toHaveTitle(/EXAMPLE/);
});

test('Second test', async ({ page }) => {
  // page is also already on the EXAMPLE homepage
  // This is a brand new context; cookies/storage from the previous test will not persist
  await expect(page.locator('nav')).toBeVisible();
});

test.beforeAll() and test.afterAll()

beforeAllInIn the current scope,runs once before all tests,afterAlland runs once after all tests.

Example

test.describe('User management', () => {
  test.beforeAll(async ({ browser }) => {
    // Create a test user before all tests (executed only once)
    // Note: beforeAll cannot use the page fixture
  });

  test.afterAll(async ({ browser }) => {
    // Delete the test user after all tests (executed only once)
  });

  test('View user list', async ({ page }) => { /* ... */ });
  test('Edit user information', async ({ page }) => { /* ... */ });
});

beforeAllandafterAllcannot accesspagethe fixture, because page is independent for each test. If you need to operate on a page, usebeforeEach。


Hook Scope and Execution Order

The scope of hooks depends on where they are defined:

Example

// File-level hook — applies to the entire file

test.beforeEach(async () => {
  console.log('File-level beforeEach');
});

test.describe('Group A', () => {
  // Group-level hook — applies only to Group A
  test.beforeEach(async () => {
    console.log("Group A's beforeEach");
  });

  test('Test A1', async () => { /* ... */ });
  test('Test A2', async () => { /* ... */ });
});

test.describe('Group B', () => {
  // Group B has no hooks of its own; only the file-level hook takes effect
  test('Test B1', async () => { /* ... */ });
});

Execution order: outer hooks run first, inner hooks run after.

For A1 and A2: first output"文件级 beforeEach", then output"分组 A 的 beforeEach"。

For B1: only output"文件级 beforeEach"。

Other Extensions