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