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
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('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
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
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
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
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
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
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.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
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"。