现在位置: 首页 > Playwright 教程 > 正文

Playwright 测试结构与钩子

本章介绍 Playwright 测试的组织方式,包括测试分组、执行控制以及生命周期钩子的用法。


test.describe() 测试分组

test.describe() 用于将相关的测试组织在一起,形成逻辑分组。

实例

// 文件路径:tests/describe-demo.spec.ts
import { test, expect } from '@playwright/test';

// 第一层分组
test.describe('RUNOOB 首页测试', () => {

  test('页面标题正确', async ({ page }) => {
    await page.goto('https://www.runoob.com/');
    await expect(page).toHaveTitle(/RUNOOB/);
  });

  test('导航栏存在', async ({ page }) => {
    await page.goto('https://www.runoob.com/');
    // 断言导航元素存在
    await expect(page.locator('nav')).toBeVisible();
  });
});

// 可以定义多个 describe 块
test.describe('搜索功能测试', () => {

  test('搜索框可见', async ({ page }) => {
    await page.goto('https://www.runoob.com/');
    await expect(
      page.getByPlaceholder('搜索')
    ).toBeVisible();
  });
});

测试报告会按照 describe 分组显示,方便查看结果。


嵌套 describe

test.describe() 可以嵌套,形成树状结构:

实例

test.describe('用户模块', () => {

  test.describe('登录功能', () => {
    test('正确账号密码登录', async ({ page }) => { /* ... */ });
    test('错误密码登录失败', async ({ page }) => { /* ... */ });
  });

  test.describe('注册功能', () => {
    test('填写正确信息注册', async ({ page }) => { /* ... */ });
    test('已存在的邮箱注册失败', async ({ page }) => { /* ... */ });
  });
});

test.skip() 跳过测试

test.skip() 用于跳过某个测试,被跳过的测试不会执行。

实例

// 直接跳过(常用于临时禁用失败的功能测试)
test.skip('尚未完成的功能测试', async ({ page }) => {
  // 这个测试不会执行
});

// 条件跳过(运行时判断)
test('在特定条件下跳过', async ({ page, browserName }) => {
  // 在 Firefox 中跳过
  test.skip(browserName === 'firefox', 'Firefox 暂不支持此功能');

  // 测试逻辑仅在非 Firefox 浏览器中执行
  await page.goto('https://www.runoob.com/');
});

test.fail() 标记预期失败

test.fail() 用于标记一个已知会失败的测试。

实例

// 标记为预期失败(不会报告为测试失败)
test.fail('已知 Bug:搜索功能暂未实现', async ({ page }) => {
  await page.goto('https://www.runoob.com/');
  // 这个断言预期会失败
  await expect(page.locator('.search-results')).toBeVisible();
});

// 如果测试意外通过了,会报告为失败
// 这样能提醒你"Bug 已修复,可以移除 test.fail 标记了"

test.fail() 的作用是反向标记:测试失败=符合预期,测试通过=异常(可能需要移除标记)。


test.only() 只运行当前测试

test.only() 用于在开发调试时只运行特定测试,忽略文件中的其他测试。

实例

// 在开发调试时,只运行当前测试
test.only('我正在调试这个测试', async ({ page }) => {
  // 只有这个测试会运行
  await page.goto('https://www.runoob.com/');
});

test('这个测试会被忽略', async ({ page }) => {
  // 由于上面有 test.only(),这个不会运行
});

test.only() 是调试工具,不应提交到代码仓库。如果你在 playwright.config.ts 中设置了 forbidOnly: true,CI 上会检测到 only 并让构建失败。


test.fixme() 标记待修复

test.fixme()test.skip() 类似,但语义上表示"这个测试需要修复"。

实例

// 标记为需要修复(不会运行)
test.fixme('登录流程因 API 变更需要更新', async ({ page }) => {
  // 这个测试暂时不会执行
});

test.slow() 标记慢测试

test.slow() 将测试的超时时间增加 3 倍。

实例

// 标记为慢测试,超时时间翻 3 倍
test.slow('大数据量测试', async ({ page }) => {
  // 这个测试的默认超时将是 90 秒(30秒 × 3)
  // 适合需要大量数据加载或复杂计算的场景
});

测试钩子(Hooks)

钩子函数让你在测试的不同阶段执行准备和清理工作。

test.beforeEach() 与 test.afterEach()

beforeEach 在每个测试前执行,afterEach 在每个测试后执行。

实例

// 文件路径:tests/hooks-demo.spec.ts
import { test, expect } from '@playwright/test';

test.beforeEach(async ({ page }) => {
  // 每个测试前都导航到同一页面
  await page.goto('https://www.runoob.com/');
});

test.afterEach(async ({ page }) => {
  // 每个测试后都执行清理(如清除创建的数据)
  // Playwright 会自动清理浏览器 context,
  // 这里的清理主要指服务端状态
  console.log('测试完成,开始清理...');
});

test('第一个测试', async ({ page }) => {
  // page 已经在 RUNOOB 首页了
  await expect(page).toHaveTitle(/RUNOOB/);
});

test('第二个测试', async ({ page }) => {
  // page 同样已经在 RUNOOB 首页
  // 这是一个全新的 context,前一个测试的 Cookie/Storage 不会残留
  await expect(page.locator('nav')).toBeVisible();
});

test.beforeAll() 与 test.afterAll()

beforeAll当前作用域所有测试之前执行一次,afterAll 在所有测试之后执行一次。

实例

test.describe('用户管理', () => {
  test.beforeAll(async ({ browser }) => {
    // 在所有测试之前创建测试用户(只执行一次)
    // 注意:beforeAll 不能使用 page fixture
  });

  test.afterAll(async ({ browser }) => {
    // 在所有测试之后删除测试用户(只执行一次)
  });

  test('查看用户列表', async ({ page }) => { /* ... */ });
  test('编辑用户信息', async ({ page }) => { /* ... */ });
});

beforeAllafterAll 不能访问 page fixture,因为 page 是每个测试独立的。如果需要操作页面,请使用 beforeEach


钩子的作用域与执行顺序

钩子的作用域取决于它们所在的位置:

实例

// 文件级钩子 —— 作用于整个文件

test.beforeEach(async () => {
  console.log('文件级 beforeEach');
});

test.describe('分组 A', () => {
  // 分组级钩子 —— 仅作用于分组 A
  test.beforeEach(async () => {
    console.log('分组 A 的 beforeEach');
  });

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

test.describe('分组 B', () => {
  // 分组 B 没有自己的钩子,只有文件级钩子生效
  test('测试 B1', async () => { /* ... */ });
});

执行顺序:外层钩子先执行,内层钩子后执行。

对于 A1 和 A2:先输出 "文件级 beforeEach",再输出 "分组 A 的 beforeEach"

对于 B1:只输出 "文件级 beforeEach"