Playwright Fixtures 与参数化
本章介绍 Playwright 的 Fixture 机制、如何创建自定义 Fixture 以及参数化测试的技巧。
什么是 Fixture
Fixture(夹具)是 Playwright 测试函数参数的依赖注入机制。
每个测试通过解构参数的方式获取所需的 Fixture(如 { page }),Playwright 负责创建和清理。
实例
// page 和 context 是内置 Fixture
test('使用内置 Fixture', async ({ page, context }) => {
// Playwright 自动创建 page 和 context
// 测试结束后自动清理
await page.goto('https://www.runoob.com/');
});
test('使用内置 Fixture', async ({ page, context }) => {
// Playwright 自动创建 page 和 context
// 测试结束后自动清理
await page.goto('https://www.runoob.com/');
});
内置 Fixture 速查
| Fixture | 类型 | 说明 |
|---|---|---|
page | Page | 独立的浏览器标签页 |
context | BrowserContext | 浏览器上下文 |
browser | Browser | 浏览器实例 |
browserName | string | 当前浏览器名称 |
request | APIRequestContext | HTTP 请求客户端 |
自定义 Fixture
通过 test.extend() 创建自定义 Fixture,可以注入额外的依赖或覆盖内置 Fixture。
实例
// 文件路径:tests/fixtures.ts
import { test as base, expect } from '@playwright/test';
// 定义自定义 Fixture 的类型
type MyFixtures = {
todoPage: { goto: () => Promise<void>; addTodo: (text: string) => Promise<void> };
authenticatedPage: any;
};
export const test = base.extend<MyFixtures>({
// 自定义 Fixture:Todo 页面对象
todoPage: async ({ page }, use) => {
// 创建
const todoPage = {
goto: async () => {
await page.goto('https://demo.playwright.dev/todomvc');
},
addTodo: async (text: string) => {
await page.getByPlaceholder('What needs to be done?').fill(text);
await page.keyboard.press('Enter');
},
};
// 注入到测试中
await use(todoPage);
// 清理逻辑(如果需要)
},
// 自定义 Fixture:已认证的页面
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
export { expect };
import { test as base, expect } from '@playwright/test';
// 定义自定义 Fixture 的类型
type MyFixtures = {
todoPage: { goto: () => Promise<void>; addTodo: (text: string) => Promise<void> };
authenticatedPage: any;
};
export const test = base.extend<MyFixtures>({
// 自定义 Fixture:Todo 页面对象
todoPage: async ({ page }, use) => {
// 创建
const todoPage = {
goto: async () => {
await page.goto('https://demo.playwright.dev/todomvc');
},
addTodo: async (text: string) => {
await page.getByPlaceholder('What needs to be done?').fill(text);
await page.keyboard.press('Enter');
},
};
// 注入到测试中
await use(todoPage);
// 清理逻辑(如果需要)
},
// 自定义 Fixture:已认证的页面
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
export { expect };
使用自定义 Fixture
实例
// 文件路径:tests/todo.spec.ts
import { test, expect } from './fixtures';
test('添加 Todo', async ({ todoPage }) => {
await todoPage.goto();
await todoPage.addTodo('学习 Playwright');
await todoPage.addTodo('掌握 Fixtures');
// 验证...
});
import { test, expect } from './fixtures';
test('添加 Todo', async ({ todoPage }) => {
await todoPage.goto();
await todoPage.addTodo('学习 Playwright');
await todoPage.addTodo('掌握 Fixtures');
// 验证...
});
Fixture 的作用域
Fixture 有三种作用域,通过 scope 选项控制。
| 作用域 | 值 | 生命周期 |
|---|---|---|
| 测试级(默认) | scope: 'test' | 每个测试创建一次 |
| Worker 级 | scope: 'worker' | 每个 Worker 进程创建一次,同一 Worker 内所有测试共享 |
实例
// Worker 级 Fixture(同一 Worker 内所有测试共享)
const test = base.extend({
sharedResource: [async ({}, use) => {
const resource = await createExpensiveResource();
await use(resource);
await resource.dispose();
}, { scope: 'worker' }], // 注意数组语法
});
const test = base.extend({
sharedResource: [async ({}, use) => {
const resource = await createExpensiveResource();
await use(resource);
await resource.dispose();
}, { scope: 'worker' }], // 注意数组语法
});
Worker 级 Fixture 在同一 Worker 进程内的多个测试间共享,适合数据库连接等创建开销大的资源。
覆盖内置 Fixture
你可以覆盖内置 Fixture 来改变默认行为。
实例
const test = base.extend({
// 覆盖内置的 page fixture,改变默认视口
page: async ({ baseURL, page }, use) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await use(page);
},
// 覆盖 storageState
storageState: 'playwright/.auth/admin.json',
});
// 覆盖内置的 page fixture,改变默认视口
page: async ({ baseURL, page }, use) => {
await page.setViewportSize({ width: 1920, height: 1080 });
await use(page);
},
// 覆盖 storageState
storageState: 'playwright/.auth/admin.json',
});
Fixture 自动清理
Fixture 的清理逻辑在 use() 调用之后执行,类似于 afterEach 的效果。
实例
const test = base.extend({
tempUser: async ({ request }, use) => {
// 创建临时用户
const resp = await request.post('/api/users', {
data: { name: 'temp_' + Date.now() },
});
const user = await resp.json();
// 注入到测试
await use(user);
// 自动清理:删除临时用户
await request.delete(`/api/users/${user.id}`);
},
});
tempUser: async ({ request }, use) => {
// 创建临时用户
const resp = await request.post('/api/users', {
data: { name: 'temp_' + Date.now() },
});
const user = await resp.json();
// 注入到测试
await use(user);
// 自动清理:删除临时用户
await request.delete(`/api/users/${user.id}`);
},
});
参数化测试
Playwright 没有内置的参数化机制,但可以通过循环动态生成测试。
实例
// 数据驱动测试
const testCases = [
{ username: 'admin', password: 'admin123', expected: '仪表盘' },
{ username: 'user', password: 'user123', expected: '个人中心' },
{ username: 'guest', password: 'guest123', expected: '访客页面' },
];
for (const { username, password, expected } of testCases) {
test(`用户 ${username} 登录后看到 ${expected}`, async ({ page }) => {
await page.goto('/login');
await page.getByLabel('用户名').fill(username);
await page.getByLabel('密码').fill(password);
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByText(expected)).toBeVisible();
});
}
const testCases = [
{ username: 'admin', password: 'admin123', expected: '仪表盘' },
{ username: 'user', password: 'user123', expected: '个人中心' },
{ username: 'guest', password: 'guest123', expected: '访客页面' },
];
for (const { username, password, expected } of testCases) {
test(`用户 ${username} 登录后看到 ${expected}`, async ({ page }) => {
await page.goto('/login');
await page.getByLabel('用户名').fill(username);
await page.getByLabel('密码').fill(password);
await page.getByRole('button', { name: '登录' }).click();
await expect(page.getByText(expected)).toBeVisible();
});
}
测试注解(Annotations)
注解用于给测试添加元数据,在报告器中可以查看和过滤。
实例
test('带有注解的测试', async ({ page }) => {
// 添加注解(在测试函数体内)
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/runoob/bug/123',
});
test.info().annotations.push({
type: 'description',
description: '这个测试覆盖了登录流程的所有分支',
});
// 测试逻辑...
});
// 使用 Tag(在测试名称中)
test('快速冒烟测试 @smoke @critical', async ({ page }) => {
// 可通过 npx playwright test --grep "@smoke" 只运行此测试
});
// 添加注解(在测试函数体内)
test.info().annotations.push({
type: 'issue',
description: 'https://github.com/runoob/bug/123',
});
test.info().annotations.push({
type: 'description',
description: '这个测试覆盖了登录流程的所有分支',
});
// 测试逻辑...
});
// 使用 Tag(在测试名称中)
test('快速冒烟测试 @smoke @critical', async ({ page }) => {
// 可通过 npx playwright test --grep "@smoke" 只运行此测试
});
