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 和标签管理来提高测试代码的可维护性。