Playwright夹具与钩子Skill playwright-fixtures-and-hooks

本技能教授如何使用Playwright测试框架的夹具系统和生命周期钩子来管理测试状态、创建可重用基础设施、实现高效测试设置和拆卸。适用于前端开发测试自动化,关键词包括:Playwright、夹具、钩子、测试自动化、前端测试、测试框架。

测试 0 次安装 0 次浏览 更新于 3/25/2026

名称: playwright-fixtures-and-hooks 用户可调用: false 描述: 在使用可重用Playwright夹具和生命周期钩子管理测试状态和基础设施时使用,以实现高效的测试设置和拆卸。 允许工具:

  • Bash
  • Read
  • Write
  • Edit

Playwright 夹具与钩子

掌握Playwright的夹具系统和生命周期钩子,以创建可重用的测试基础设施,管理测试状态,并构建可维护的测试套件。 此技能涵盖内置夹具、自定义夹具以及测试设置和拆卸的最佳实践。

内置夹具

核心夹具

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

test('使用内置夹具', async ({
  page,      // 页面实例
  context,   // 浏览器上下文
  browser,   // 浏览器实例
  request,   // API请求上下文
}) => {
  // 每个测试获取新的页面和上下文
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

页面夹具

test('页面夹具示例', async ({ page }) => {
  // 导航
  await page.goto('https://example.com');

  // 交互
  await page.getByRole('button', { name: '点击我' }).click();

  // 等待
  await page.waitForLoadState('networkidle');

  // 评估
  const title = await page.title();
  expect(title).toBe('Example Domain');
});

上下文夹具

test('上下文夹具示例', async ({ context, page }) => {
  // 添加Cookie
  await context.addCookies([
    {
      name: 'session',
      value: 'abc123',
      domain: 'example.com',
      path: '/',
    },
  ]);

  // 设置权限
  await context.grantPermissions(['geolocation']);

  // 在同一上下文中创建额外页面
  const page2 = await context.newPage();
  await page2.goto('https://example.com');

  // 两个页面共享Cookie和存储
  await page.goto('https://example.com');
});

浏览器夹具

test('浏览器夹具示例', async ({ browser }) => {
  // 使用选项创建自定义上下文
  const context = await browser.newContext({
    viewport: { width: 1920, height: 1080 },
    locale: 'en-US',
    timezoneId: 'America/New_York',
    permissions: ['geolocation'],
  });

  const page = await context.newPage();
  await page.goto('https://example.com');

  await context.close();
});

请求夹具

test('使用请求夹具进行API测试', async ({ request }) => {
  // 发起GET请求
  const response = await request.get('https://api.example.com/users');
  expect(response.ok()).toBeTruthy();
  expect(response.status()).toBe(200);

  const users = await response.json();
  expect(users).toHaveLength(10);

  // 发起POST请求
  const createResponse = await request.post('https://api.example.com/users', {
    data: {
      name: 'John Doe',
      email: 'john@example.com',
    },
  });
  expect(createResponse.ok()).toBeTruthy();
});

自定义夹具

基本自定义夹具

// fixtures/base-fixtures.ts
import { test as base } from '@playwright/test';

type MyFixtures = {
  timestamp: string;
};

export const test = base.extend<MyFixtures>({
  timestamp: async ({}, use) => {
    const timestamp = new Date().toISOString();
    await use(timestamp);
  },
});

export { expect } from '@playwright/test';
// tests/example.spec.ts
import { test, expect } from '../fixtures/base-fixtures';

test('使用自定义时间戳夹具', async ({ timestamp, page }) => {
  console.log(`测试开始于: ${timestamp}`);
  await page.goto('https://example.com');
});

带有设置和拆卸的夹具

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

type DatabaseFixtures = {
  database: Database;
};

export const test = base.extend<DatabaseFixtures>({
  database: async ({}, use) => {
    // 设置: 创建数据库连接
    const db = await createDatabaseConnection();
    console.log('数据库已连接');

    // 提供夹具给测试
    await use(db);

    // 拆卸: 关闭数据库连接
    await db.close();
    console.log('数据库已关闭');
  },
});

夹具作用域: 测试与工作者

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

type TestScopedFixtures = {
  uniqueId: string;
};

type WorkerScopedFixtures = {
  apiToken: string;
};

export const test = base.extend<TestScopedFixtures, WorkerScopedFixtures>({
  // 测试作用域: 为每个测试创建
  uniqueId: async ({}, use) => {
    const id = `test-${Date.now()}-${Math.random()}`;
    await use(id);
  },

  // 工作者作用域: 每个工作者创建一次
  apiToken: [
    async ({}, use) => {
      const token = await generateApiToken();
      await use(token);
      await revokeApiToken(token);
    },
    { scope: 'worker' },
  ],
});

认证夹具

认证用户夹具

// fixtures/auth-fixtures.ts
import { test as base } from '@playwright/test';

type AuthFixtures = {
  authenticatedPage: Page;
};

export const test = base.extend<AuthFixtures>({
  authenticatedPage: async ({ browser }, use) => {
    // 创建带认证的新上下文
    const context = await browser.newContext({
      storageState: 'auth.json',
    });

    const page = await context.newPage();
    await use(page);

    await context.close();
  },
});

export { expect } from '@playwright/test';

多用户角色

// fixtures/multi-user-fixtures.ts
import { test as base } from '@playwright/test';

type UserFixtures = {
  adminPage: Page;
  userPage: Page;
  guestPage: Page;
};

export const test = base.extend<UserFixtures>({
  adminPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'auth/admin.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },

  userPage: async ({ browser }, use) => {
    const context = await browser.newContext({
      storageState: 'auth/user.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },

  guestPage: async ({ browser }, use) => {
    const context = await browser.newContext();
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
});

认证设置

// auth/setup.ts
import { test as setup } from '@playwright/test';

setup('作为管理员认证', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Email').fill('admin@example.com');
  await page.getByLabel('Password').fill('admin123');
  await page.getByRole('button', { name: '登录' }).click();

  await page.waitForURL('**/dashboard');

  await page.context().storageState({ path: 'auth/admin.json' });
});

setup('作为用户认证', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill('user123');
  await page.getByRole('button', { name: '登录' }).click();

  await page.waitForURL('**/dashboard');

  await page.context().storageState({ path: 'auth/user.json' });
});

数据库夹具

测试数据库夹具

// fixtures/database-fixtures.ts
import { test as base } from '@playwright/test';
import { PrismaClient } from '@prisma/client';

type DatabaseFixtures = {
  db: PrismaClient;
  cleanDb: void;
};

export const test = base.extend<DatabaseFixtures>({
  db: [
    async ({}, use) => {
      const db = new PrismaClient();
      await use(db);
      await db.$disconnect();
    },
    { scope: 'worker' },
  ],

  cleanDb: async ({ db }, use) => {
    // 测试前清理数据库
    await db.user.deleteMany();
    await db.product.deleteMany();
    await db.order.deleteMany();

    await use();

    // 测试后清理数据库
    await db.user.deleteMany();
    await db.product.deleteMany();
    await db.order.deleteMany();
  },
});

种子数据夹具

// fixtures/seed-fixtures.ts
import { test as base } from './database-fixtures';

type SeedFixtures = {
  testUser: User;
  testProducts: Product[];
};

export const test = base.extend<SeedFixtures>({
  testUser: async ({ db, cleanDb }, use) => {
    const user = await db.user.create({
      data: {
        email: 'test@example.com',
        name: '测试用户',
        password: 'hashedpassword',
      },
    });

    await use(user);
  },

  testProducts: async ({ db, cleanDb }, use) => {
    const products = await db.product.createMany({
      data: [
        { name: '产品 1', price: 10.99 },
        { name: '产品 2', price: 20.99 },
        { name: '产品 3', price: 30.99 },
      ],
    });

    const allProducts = await db.product.findMany();
    await use(allProducts);
  },
});

API模拟夹具

模拟API夹具

// fixtures/mock-api-fixtures.ts
import { test as base } from '@playwright/test';

type MockApiFixtures = {
  mockApi: void;
};

export const test = base.extend<MockApiFixtures>({
  mockApi: async ({ page }, use) => {
    // 模拟API响应
    await page.route('**/api/users', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: '用户 1' },
          { id: 2, name: '用户 2' },
        ]),
      });
    });

    await page.route('**/api/products', async (route) => {
      await route.fulfill({
        status: 200,
        contentType: 'application/json',
        body: JSON.stringify([
          { id: 1, name: '产品 1', price: 10 },
          { id: 2, name: '产品 2', price: 20 },
        ]),
      });
    });

    await use();

    // 清理: 取消所有路由
    await page.unrouteAll();
  },
});

条件模拟

// fixtures/conditional-mock-fixtures.ts
import { test as base } from '@playwright/test';

type ConditionalMockFixtures = {
  mockFailedApi: void;
  mockSlowApi: void;
};

export const test = base.extend<ConditionalMockFixtures>({
  mockFailedApi: async ({ page }, use) => {
    await page.route('**/api/**', async (route) => {
      await route.fulfill({
        status: 500,
        contentType: 'application/json',
        body: JSON.stringify({ error: '内部服务器错误' }),
      });
    });

    await use();
    await page.unrouteAll();
  },

  mockSlowApi: async ({ page }, use) => {
    await page.route('**/api/**', async (route) => {
      // 模拟慢速网络
      await new Promise((resolve) => setTimeout(resolve, 3000));
      await route.continue();
    });

    await use();
    await page.unrouteAll();
  },
});

生命周期钩子

测试钩子

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

test.describe('用户管理', () => {
  test.beforeAll(async () => {
    // 在此描述块中的所有测试之前运行一次
    console.log('设置测试套件');
  });

  test.beforeEach(async ({ page }) => {
    // 每个测试之前运行
    await page.goto('https://example.com');
    console.log('测试开始');
  });

  test.afterEach(async ({ page }, testInfo) => {
    // 每个测试之后运行
    console.log(`测试 ${testInfo.status}: ${testInfo.title}`);

    if (testInfo.status !== testInfo.expectedStatus) {
      // 测试失败 - 捕获额外调试信息
      const screenshot = await page.screenshot();
      await testInfo.attach('失败截图', {
        body: screenshot,
        contentType: 'image/png',
      });
    }
  });

  test.afterAll(async () => {
    // 在此描述块中的所有测试之后运行一次
    console.log('清理测试套件');
  });

  test('测试 1', async ({ page }) => {
    // 测试实现
  });

  test('测试 2', async ({ page }) => {
    // 测试实现
  });
});

嵌套钩子

test.describe('父套件', () => {
  test.beforeEach(async ({ page }) => {
    console.log('父beforeEach');
    await page.goto('https://example.com');
  });

  test.describe('子套件 1', () => {
    test.beforeEach(async ({ page }) => {
      console.log('子 1 beforeEach');
      await page.getByRole('link', { name: '产品' }).click();
    });

    test('子套件 1 中的测试', async ({ page }) => {
      // 先运行父beforeEach,然后子beforeEach
    });
  });

  test.describe('子套件 2', () => {
    test.beforeEach(async ({ page }) => {
      console.log('子 2 beforeEach');
      await page.getByRole('link', { name: '关于' }).click();
    });

    test('子套件 2 中的测试', async ({ page }) => {
      // 先运行父beforeEach,然后子beforeEach
    });
  });
});

条件钩子

test.describe('功能测试', () => {
  test.beforeEach(async ({ page, browserName }) => {
    // 跳过Firefox的设置
    if (browserName === 'firefox') {
      test.skip();
    }

    await page.goto('https://example.com');
  });

  test.afterEach(async ({ page }, testInfo) => {
    // 仅对失败测试运行拆卸
    if (testInfo.status === 'failed') {
      await page.screenshot({ path: `failure-${testInfo.title}.png` });
    }
  });

  test('功能测试', async ({ page }) => {
    // 测试实现
  });
});

夹具依赖

依赖夹具

// fixtures/dependent-fixtures.ts
import { test as base } from '@playwright/test';

type DependentFixtures = {
  config: Config;
  apiClient: ApiClient;
  authenticatedClient: ApiClient;
};

export const test = base.extend<DependentFixtures>({
  // 基础夹具
  config: async ({}, use) => {
    const config = {
      apiUrl: process.env.API_URL || 'http://localhost:3000',
      timeout: 30000,
    };
    await use(config);
  },

  // 依赖于config
  apiClient: async ({ config }, use) => {
    const client = new ApiClient(config.apiUrl, config.timeout);
    await use(client);
  },

  // 依赖于apiClient
  authenticatedClient: async ({ apiClient }, use) => {
    const token = await apiClient.login('user@example.com', 'password');
    apiClient.setAuthToken(token);
    await use(apiClient);
  },
});

组合多个夹具

// fixtures/combined-fixtures.ts
import { test as base } from '@playwright/test';

type CombinedFixtures = {
  setupComplete: void;
};

export const test = base.extend<CombinedFixtures>({
  setupComplete: async (
    { page, db, mockApi, testUser },
    use
  ) => {
    // 所有依赖夹具都已初始化
    await page.goto('https://example.com');
    await page.context().addCookies([
      {
        name: 'userId',
        value: testUser.id.toString(),
        domain: 'example.com',
        path: '/',
      },
    ]);

    await use();
  },
});

高级夹具模式

工厂夹具

// fixtures/factory-fixtures.ts
import { test as base } from '@playwright/test';

type FactoryFixtures = {
  createUser: (data: Partial<User>) => Promise<User>;
  createProduct: (data: Partial<Product>) => Promise<Product>;
};

export const test = base.extend<FactoryFixtures>({
  createUser: async ({ db }, use) => {
    const users: User[] = [];

    const createUser = async (data: Partial<User>) => {
      const user = await db.user.create({
        data: {
          email: data.email || `user-${Date.now()}@example.com`,
          name: data.name || '测试用户',
          password: data.password || 'password123',
          ...data,
        },
      });
      users.push(user);
      return user;
    };

    await use(createUser);

    // 清理: 删除所有创建的用户
    for (const user of users) {
      await db.user.delete({ where: { id: user.id } });
    }
  },

  createProduct: async ({ db }, use) => {
    const products: Product[] = [];

    const createProduct = async (data: Partial<Product>) => {
      const product = await db.product.create({
        data: {
          name: data.name || `产品 ${Date.now()}`,
          price: data.price || 9.99,
          description: data.description || '测试产品',
          ...data,
        },
      });
      products.push(product);
      return product;
    };

    await use(createProduct);

    // 清理: 删除所有创建的产品
    for (const product of products) {
      await db.product.delete({ where: { id: product.id } });
    }
  },
});

选项夹具

// fixtures/option-fixtures.ts
import { test as base } from '@playwright/test';

type OptionsFixtures = {
  slowNetwork: boolean;
};

export const test = base.extend<OptionsFixtures>({
  slowNetwork: [false, { option: true }],

  page: async ({ page, slowNetwork }, use) => {
    if (slowNetwork) {
      await page.route('**/*', async (route) => {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        await route.continue();
      });
    }

    await use(page);
  },
});
// tests/slow-network.spec.ts
import { test, expect } from '../fixtures/option-fixtures';

test('慢速网络测试', async ({ page }) => {
  test.use({ slowNetwork: true });

  await page.goto('https://example.com');
  // 由于网络限制,这将很慢
});

test('正常网络测试', async ({ page }) => {
  await page.goto('https://example.com');
  // 正常速度
});

测试信息和附件

使用测试信息

test('使用测试信息的示例', async ({ page }, testInfo) => {
  console.log(`测试标题: ${testInfo.title}`);
  console.log(`项目: ${testInfo.project.name}`);
  console.log(`重试: ${testInfo.retry}`);

  await page.goto('https://example.com');

  // 附加截图
  const screenshot = await page.screenshot();
  await testInfo.attach('页面截图', {
    body: screenshot,
    contentType: 'image/png',
  });

  // 附加JSON数据
  await testInfo.attach('测试数据', {
    body: JSON.stringify({ foo: 'bar' }),
    contentType: 'application/json',
  });

  // 附加文本
  await testInfo.attach('备注', {
    body: '测试成功完成',
    contentType: 'text/plain',
  });
});

条件测试执行

test('浏览器特定测试', async ({ page, browserName }) => {
  test.skip(browserName === 'webkit', 'Safari中不支持');

  await page.goto('https://example.com');
  // 测试仅在Chromium和Firefox中运行
});

test('慢速测试', async ({ page }) => {
  test.slow(); // 将此测试的超时时间增加三倍

  await page.goto('https://slow-site.example.com');
  // 长时间运行的操作
});

test('预期失败', async ({ page }) => {
  test.fail(); // 标记为预期失败

  await page.goto('https://example.com');
  await expect(page.getByText('不存在的')).toBeVisible();
});

夹具最佳实践

组织夹具

fixtures/
├── index.ts              # 导出所有夹具
├── auth-fixtures.ts      # 认证夹具
├── database-fixtures.ts  # 数据库夹具
├── mock-api-fixtures.ts  # API模拟夹具
└── page-fixtures.ts      # 页面相关夹具
// fixtures/index.ts
import { test as authTest } from './auth-fixtures';
import { test as dbTest } from './database-fixtures';
import { test as mockTest } from './mock-api-fixtures';

export const test = authTest.extend(dbTest.fixtures).extend(mockTest.fixtures);

export { expect } from '@playwright/test';

夹具命名约定

// 好的命名
export const test = base.extend({
  authenticatedPage: async ({}, use) => { /* ... */ },
  testUser: async ({}, use) => { /* ... */ },
  mockApi: async ({}, use) => { /* ... */ },
});

// 避免
export const test = base.extend({
  page2: async ({}, use) => { /* ... */ },  // 不具描述性
  data: async ({}, use) => { /* ... */ },   // 太通用
  fixture1: async ({}, use) => { /* ... */ }, // 无意义名称
});

何时使用此技能

  • 设置可重用的测试基础设施
  • 跨测试管理认证状态
  • 创建数据库种子和清理逻辑
  • 为测试实现API模拟
  • 构建测试数据生成的工厂夹具
  • 建立测试生命周期模式
  • 创建性能优化的工作者作用域夹具
  • 组织复杂的测试设置和拆卸
  • 实现条件测试行为
  • 构建类型安全的夹具系统

资源