Playwright 第9章:Playwright Test 框架

@playwright/test 是 Playwright 官方提供的测试框架,集成了测试运行、断言、报告和并行执行等功能。本章系统介绍 test()、describe() 的使用方法、断言库、测试报告和并行执行策略。

测试框架概述

安装

npm init playwright@latest
# 或
npm install -D @playwright/test
npx playwright install

基本测试结构

import { test, expect } from '@playwright/test';

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

test() 方法详解

基本用法

import { test, expect } from '@playwright/test';

// 最简形式
test('基本测试', async ({ page }) => {
  await page.goto('/');
  await expect(page.locator('h1')).toBeVisible();
});

// 异步测试
test('异步操作测试', async ({ page }) => {
  await page.goto('/');
  const result = await page.evaluate(async () => {
    const response = await fetch('/api/data');
    return response.json();
  });
  expect(result.success).toBe(true);
});

// 回调风格(不推荐)
test('回调测试', ({ page }, done) => {
  page.on('dialog', async (dialog) => {
    expect(dialog.message()).toBe('Hello');
    await dialog.accept();
    done();
  });
  page.click('#show-alert');
});

test 选项

// 设置测试超时
test('超时测试', { timeout: 60000 }, async ({ page }) => {
  await page.goto('/slow-page');
  await expect(page.locator('.content')).toBeVisible();
});

// 跳过测试
test.skip('跳过的测试', async ({ page }) => {
  // 此测试不会执行
});

// 条件跳过
test('条件跳过', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'WebKit 不支持此功能');
  await page.goto('/');
  // WebKit 下不执行
});

// 标记为预期失败
test.fail('预期失败的测试', async ({ page }) => {
  // 此测试失败不会导致整体失败
});

// 设置重试次数
test('重试测试', { retries: 3 }, async ({ page }) => {
  await page.goto('/flaky-page');
});

describe() 分组

基本分组

import { test, expect } from '@playwright/test';

test.describe('用户登录功能', () => {
  test('正确用户名和密码登录成功', async ({ page }) => {
    await page.goto('/login');
    await page.fill('#username', 'admin');
    await page.fill('#password', 'correct-password');
    await page.click('button[type="submit"]');
    await expect(page).toHaveURL('/dashboard');
  });

  test('错误密码显示错误提示', async ({ page }) => {
    await page.goto('/login');
    await page.fill('#username', 'admin');
    await page.fill('#password', 'wrong-password');
    await page.click('button[type="submit"]');
    await expect(page.locator('.error-message')).toBeVisible();
  });
});

嵌套分组

test.describe('电商网站测试', () => {
  test.describe('商品浏览', () => {
    test('商品列表展示正常', async ({ page }) => {
      await page.goto('/products');
      await expect(page.locator('.product-card')).toHaveCount(20);
    });

    test('商品搜索功能', async ({ page }) => {
      await page.goto('/products');
      await page.fill('#search', '手机');
      await page.click('.search-btn');
      await expect(page.locator('.product-card')).toHaveCount(5);
    });
  });

  test.describe('购物车功能', () => {
    test('添加商品到购物车', async ({ page }) => {
      await page.goto('/products/1');
      await page.click('.add-to-cart');
      const badge = page.locator('.cart-badge');
      await expect(badge).toHaveText('1');
    });
  });
});

分组配置

// 给分组添加标签
test.describe('用户管理 @smoke', () => {
  test('创建用户', async ({ page }) => {
    // ...
  });

  test('删除用户', async ({ page }) => {
    // ...
  });
});

// 条件分组
test.describe('移动端测试', () => {
  test.skip(({ browserName }) => browserName !== 'chromium', '仅 Chromium');

  test('触摸操作', async ({ page }) => {
    // ...
  });
});

内置 Fixtures

常用 Fixtures

import { test, expect } from '@playwright/test';

// page - 页面对象
test('使用 page fixture', async ({ page }) => {
  await page.goto('/');
});

// context - 浏览器上下文
test('使用 context fixture', async ({ context }) => {
  const page = await context.newPage();
  await page.goto('/');
});

// browser - 浏览器实例
test('使用 browser fixture', async ({ browser }) => {
  const context = await browser.newContext();
  const page = await context.newPage();
  await page.goto('/');
});

// request - API 请求
test('使用 request fixture', async ({ request }) => {
  const response = await request.get('/api/users');
  expect(response.ok()).toBeTruthy();
  const data = await response.json();
  expect(data.users).toHaveLength(10);
});

自定义 Fixture

import { test as base, expect } from '@playwright/test';

// 定义自定义 Fixture
type MyFixtures = {
  loggedInPage: { page: Page; username: string };
};

const test = base.extend<MyFixtures>({
  loggedInPage: async ({ page }, use) => {
    // 安装:执行登录
    await page.goto('/login');
    await page.fill('#username', 'admin');
    await page.fill('#password', 'password');
    await page.click('button[type="submit"]');
    await page.waitForURL('/dashboard');

    // 提供给测试使用
    await use({ page, username: 'admin' });

    // 拆卸:清理
    await page.evaluate(() => {
      localStorage.clear();
      sessionStorage.clear();
    });
  },
});

// 使用自定义 Fixture
test('使用已登录状态', async ({ loggedInPage }) => {
  const { page, username } = loggedInPage;
  await expect(page.locator('.welcome')).toContainText(username);
});

测试报告

配置报告器

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['html'],                    // HTML 报告
    ['list'],                    // 命令行列表
    ['line'],                    // 单行报告
    ['json', { outputFile: 'results.json' }],
    ['junit', { outputFile: 'junit.xml' }],
    ['blob'],                    // 用于分片合并
  ],
});

查看报告

# 运行测试后查看 HTML 报告
npx playwright show-report

# 指定报告目录
npx playwright show-report test-results/report

并行执行

配置并行

export default defineConfig({
  // 完全并行
  fullyParallel: true,

  // Worker 数量
  workers: process.env.CI ? 4 : 2,

  // 分片执行
  // CI 中配合 shard 参数使用
});

分片执行

# 将测试分为 3 片,运行第 1 片
npx playwright test --shard=1/3

# 运行第 2 片
npx playwright test --shard=2/3

# 运行第 3 片
npx playwright test --shard=3/3

# 合并分片报告
npx playwright merge-reports --reporter html ./shard-results/

命令行选项

# 运行指定文件
npx playwright test tests/login.spec.ts

# 运行指定项目
npx playwright test --project=chromium

# 有头模式
npx playwright test --headed

# 调试模式
npx playwright test --debug

# UI 模式
npx playwright test --ui

# 更新截图
npx playwright test --update-snapshots

# 匹配测试名称
npx playwright test -g "登录"

总结

@playwright/test 提供了完整的测试框架功能。test() 和 describe() 用于组织测试,Fixtures 提供了依赖注入能力,丰富的报告器满足不同需求。并行执行和分片策略显著提升了大规模测试集的运行效率。在实际项目中,建议合理使用自定义 Fixture 和标签管理来提高测试代码的可维护性。