Playwright 测试夹具 #
Fixtures 概述 #
Fixtures 是 Playwright 的核心机制,用于为测试提供所需的环境、数据和对象。每个测试都会获得独立的夹具实例,确保测试隔离。
Fixtures 的优势 #
text
┌─────────────────────────────────────────────────────────────┐
│ Fixtures 优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 测试隔离 - 每个测试获得独立的夹具实例 │
│ ✅ 按需创建 - 只在需要时创建夹具 │
│ ✅ 自动清理 - 测试结束后自动清理 │
│ ✅ 类型安全 - TypeScript 完整支持 │
│ ✅ 组合复用 - 夹具可以依赖其他夹具 │
│ │
└─────────────────────────────────────────────────────────────┘
工作原理 #
text
测试开始
│
├── 解析所需夹具
│
├── 创建夹具实例
│ ├── 夹具 A
│ ├── 夹具 B (依赖 A)
│ └── 夹具 C (依赖 A, B)
│
├── 执行测试
│
└── 清理夹具(逆序)
├── 清理 C
├── 清理 B
└── 清理 A
内置夹具 #
常用内置夹具 #
typescript
import { test, expect } from '@playwright/test';
test('内置夹具示例', async ({
page, // Page 对象
context, // BrowserContext
browser, // Browser 实例
browserName, // 浏览器名称
request, // APIRequestContext
}) => {
// 使用 page
await page.goto('/');
// 使用 context
const newPage = await context.newPage();
// 使用 request
const response = await request.get('/api/users');
// 使用 browserName
console.log(`当前浏览器: ${browserName}`);
});
内置夹具列表 #
| 夹具 | 类型 | 描述 |
|---|---|---|
page |
Page | 独立的浏览器页面 |
context |
BrowserContext | 独立的浏览器上下文 |
browser |
Browser | 浏览器实例 |
browserName |
string | 当前浏览器名称 |
request |
APIRequestContext | API 请求上下文 |
baseURL |
string | 基础 URL |
projectName |
string | 项目名称 |
自定义夹具 #
基本自定义夹具 #
typescript
// fixtures.ts
import { test as base, expect } from '@playwright/test';
// 定义夹具类型
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// 扩展测试
export const test = base.extend<MyFixtures>({
// 定义 todoPage 夹具
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage);
// 清理代码(可选)
},
// 定义 settingsPage 夹具
settingsPage: async ({ page }, use) => {
const settingsPage = new SettingsPage(page);
await use(settingsPage);
},
});
// 导出 expect
export { expect };
使用自定义夹具 #
typescript
// tests/todo.spec.ts
import { test, expect } from '../fixtures';
test('添加待办事项', async ({ todoPage }) => {
await todoPage.addTodo('Buy milk');
await expect(todoPage.todoItems).toHaveCount(1);
});
test('删除待办事项', async ({ todoPage }) => {
await todoPage.addTodo('Buy milk');
await todoPage.deleteTodo(0);
await expect(todoPage.todoItems).toHaveCount(0);
});
夹具依赖 #
夹具之间的依赖 #
typescript
// fixtures.ts
import { test as base, expect } from '@playwright/test';
type MyFixtures = {
authenticatedPage: Page;
user: { id: number; name: string; email: string };
};
export const test = base.extend<MyFixtures>({
// 认证夹具
authenticatedPage: async ({ page }, use) => {
// 登录
await page.goto('/login');
await page.fill('#email', 'test@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
// 提供已认证的页面
await use(page);
// 清理:登出
await page.click('#logout');
},
// 用户数据夹具
user: async ({ request }, use) => {
// 创建测试用户
const response = await request.post('/api/users', {
data: {
name: 'Test User',
email: 'test@example.com',
},
});
const user = await response.json();
await use(user);
// 清理:删除用户
await request.delete(`/api/users/${user.id}`);
},
});
使用依赖夹具 #
typescript
// tests/profile.spec.ts
import { test, expect } from '../fixtures';
test('查看用户资料', async ({ authenticatedPage, user }) => {
await authenticatedPage.goto('/profile');
await expect(authenticatedPage.locator('.user-name')).toHaveText(user.name);
await expect(authenticatedPage.locator('.user-email')).toHaveText(user.email);
});
夹具选项 #
参数化夹具 #
typescript
// fixtures.ts
import { test as base } from '@playwright/test';
type Account = {
username: string;
password: string;
role: 'admin' | 'user' | 'guest';
};
type MyFixtures = {
account: Account;
};
export const test = base.extend<MyFixtures>({
// 带默认值的夹具
account: [async ({}, use) => {
await use({
username: 'default_user',
password: 'default_password',
role: 'user',
});
}, { option: true }],
});
在配置中覆盖夹具选项 #
typescript
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'admin-tests',
use: {
account: {
username: 'admin',
password: 'admin_password',
role: 'admin',
},
},
},
{
name: 'user-tests',
use: {
account: {
username: 'regular_user',
password: 'user_password',
role: 'user',
},
},
},
],
});
夹具生命周期 #
setup 和 teardown #
typescript
// fixtures.ts
import { test as base, expect } from '@playwright/test';
type MyFixtures = {
database: DatabaseConnection;
testUser: User;
};
export const test = base.extend<MyFixtures>({
database: async ({}, use) => {
// Setup - 创建连接
const db = new DatabaseConnection();
await db.connect();
// 提供给测试使用
await use(db);
// Teardown - 关闭连接
await db.disconnect();
},
testUser: async ({ database, request }, use) => {
// Setup - 创建测试用户
const response = await request.post('/api/users', {
data: { name: 'Test User', email: 'test@example.com' },
});
const user = await response.json();
await use(user);
// Teardown - 删除测试用户
await request.delete(`/api/users/${user.id}`);
},
});
复杂清理场景 #
typescript
// fixtures.ts
import { test as base } from '@playwright/test';
type MyFixtures = {
cleanUp: () => void;
cleanupRegistry: Set<() => Promise<void>>;
};
export const test = base.extend<MyFixtures>({
cleanupRegistry: async ({}, use) => {
const registry = new Set<() => Promise<void>>();
await use(registry);
// 执行所有清理函数
for (const cleanup of registry) {
await cleanup();
}
},
cleanUp: async ({ cleanupRegistry }, use) => {
const register = (fn: () => Promise<void>) => {
cleanupRegistry.add(fn);
};
await use(register);
},
});
// 使用
test('测试', async ({ cleanUp, request }) => {
const response = await request.post('/api/resources');
const resource = await response.json();
// 注册清理函数
cleanUp(async () => {
await request.delete(`/api/resources/${resource.id}`);
});
});
实用夹具示例 #
认证夹具 #
typescript
// fixtures/auth.ts
import { test as base, expect } from '@playwright/test';
type AuthFixtures = {
loggedInPage: Page;
adminPage: Page;
};
export const test = base.extend<AuthFixtures>({
loggedInPage: async ({ page }, use) => {
await page.goto('/login');
await page.fill('#email', 'user@example.com');
await page.fill('#password', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await use(page);
},
adminPage: async ({ page }, use) => {
await page.goto('/login');
await page.fill('#email', 'admin@example.com');
await page.fill('#password', 'admin_password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/admin');
await use(page);
},
});
数据夹具 #
typescript
// fixtures/data.ts
import { test as base } from '@playwright/test';
type DataFixtures = {
testProducts: Product[];
testOrders: Order[];
};
export const test = base.extend<DataFixtures>({
testProducts: async ({ request }, use) => {
// 创建测试产品
const products: Product[] = [];
for (let i = 0; i < 5; i++) {
const response = await request.post('/api/products', {
data: {
name: `Test Product ${i}`,
price: 100 * (i + 1),
},
});
products.push(await response.json());
}
await use(products);
// 清理
for (const product of products) {
await request.delete(`/api/products/${product.id}`);
}
},
testOrders: async ({ request, testProducts }, use) => {
// 创建测试订单
const orders: Order[] = [];
for (const product of testProducts.slice(0, 2)) {
const response = await request.post('/api/orders', {
data: { productId: product.id, quantity: 1 },
});
orders.push(await response.json());
}
await use(orders);
// 清理
for (const order of orders) {
await request.delete(`/api/orders/${order.id}`);
}
},
});
Mock 夹具 #
typescript
// fixtures/mock.ts
import { test as base } from '@playwright/test';
type MockFixtures = {
mockApi: (endpoint: string, response: any) => void;
};
export const test = base.extend<MockFixtures>({
mockApi: async ({ page }, use) => {
const mockEndpoint = async (endpoint: string, response: any) => {
await page.route(`**/api/${endpoint}`, route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(response),
});
});
};
await use(mockEndpoint);
// 清理所有路由
await page.unrouteAll();
},
});
// 使用
test('Mock API 测试', async ({ page, mockApi }) => {
mockApi('users', [{ id: 1, name: 'Test User' }]);
await page.goto('/users');
await expect(page.locator('.user-name')).toHaveText('Test User');
});
截图夹具 #
typescript
// fixtures/screenshot.ts
import { test as base } from '@playwright/test';
type ScreenshotFixtures = {
takeScreenshot: (name: string) => Promise<void>;
};
export const test = base.extend<ScreenshotFixtures>({
takeScreenshot: async ({ page }, use) => {
const screenshotDir = 'screenshots';
let counter = 0;
const takeScreenshot = async (name: string) => {
counter++;
await page.screenshot({
path: `${screenshotDir}/${counter}_${name}.png`,
});
};
await use(takeScreenshot);
},
});
// 使用
test('截图测试', async ({ page, takeScreenshot }) => {
await page.goto('/');
await takeScreenshot('homepage');
await page.click('button');
await takeScreenshot('after_click');
});
夹具组合 #
多个夹具文件 #
typescript
// fixtures/index.ts
import { mergeTests } from '@playwright/test';
import { test as authTest } from './auth';
import { test as dataTest } from './data';
import { test as mockTest } from './mock';
// 合并多个测试
export const test = mergeTests(authTest, dataTest, mockTest);
使用合并的夹具 #
typescript
// tests/integration.spec.ts
import { test, expect } from '../fixtures';
test('集成测试', async ({
loggedInPage, // 来自 auth 夹具
testProducts, // 来自 data 夹具
mockApi, // 来自 mock 夹具
}) => {
// 使用所有夹具
});
夹具最佳实践 #
1. 保持夹具独立 #
typescript
// ✅ 推荐 - 独立的夹具
test('测试', async ({ page, request }) => {
// page 和 request 独立
});
// ❌ 不推荐 - 在夹具中依赖全局状态
let globalState;
test('测试', async ({ page }) => {
globalState = await page.evaluate(() => window.state);
});
2. 使用 TypeScript 类型 #
typescript
// ✅ 推荐 - 明确类型
type MyFixtures = {
user: User;
products: Product[];
};
export const test = base.extend<MyFixtures>({
user: async ({ request }, use) => {
const response = await request.post('/api/users');
const user: User = await response.json();
await use(user);
},
});
3. 正确处理清理 #
typescript
// ✅ 推荐 - 确保清理执行
testFixtures: async ({ request }, use) => {
let resourceId: string;
try {
const response = await request.post('/api/resources');
resourceId = (await response.json()).id;
await use(resourceId);
} finally {
// 始终清理
if (resourceId) {
await request.delete(`/api/resources/${resourceId}`);
}
}
},
4. 避免过度使用夹具 #
typescript
// ✅ 推荐 - 简单场景直接使用
test('简单测试', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toBeVisible();
});
// ✅ 推荐 - 复杂场景使用夹具
test('复杂测试', async ({ authenticatedPage, testProducts }) => {
// 使用预配置的夹具
});
下一步 #
现在你已经掌握了测试夹具,接下来学习 网络 Mock 了解如何模拟网络请求!
最后更新:2026-03-28