自定义夹具与组合复用
本教程共 59 篇 · 第 39 篇 · 更新于 2026-08-04 · 约 11 分钟阅读
本节目标:学完能用 test.extend 造自己的夹具,把它抽到独立文件跨测试复用,并学会覆盖内置夹具来满足特殊需求。
内置夹具够用,但写业务测试时,你会反复需要「一个已经登录的页面」「一个装好测试数据的环境」。这种重复准备,正是自定义夹具的用武之地。
用 test.extend 造夹具
核心方法是 test.extend(),它返回一个「升级版」的 test,带着你新增的夹具。
// my-test.ts
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
type MyFixtures = {
todoPage: TodoPage;
};
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await use(todoPage); // 交给测试用
await todoPage.removeAll(); // 用完清理
},
});
export { expect } from '@playwright/test';
之后在测试里直接声明 todoPage 就能用:
import { test, expect } from './my-test';
test('能新增一项', async ({ todoPage }) => {
await todoPage.addToDo('something nice');
});
Note自定义夹具名只能以字母或下划线开头,且只能含字母、数字、下划线,别用连字符。
跨文件共享
把上面的 my-test.ts 放在公共位置,谁要用就 import 谁。这样所有测试共享同一套夹具定义,改一处全局生效。
import { test, expect } from '../playwright/fixtures';
如果想从多个模块合并夹具,用 mergeTests:
import { mergeTests } from '@playwright/test';
import { test as dbTest } from 'database-test-utils';
import { test as a11yTest } from 'a11y-test-utils';
export const test = mergeTests(dbTest, a11yTest);
依赖组合:夹具套夹具
夹具可以依赖别的夹具,Playwright 会按依赖关系自动排好初始化顺序。下面两个夹具都依赖内置的 page:
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
import { SettingsPage } from './settings-page';
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage; // 新夹具要先写进类型,否则 TS 会报错
};
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage);
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
Note每加一个自定义夹具,都要在
MyFixtures类型里补一条。漏了 TS 就报「类型不匹配」,这是新手最常撞的一个错。
覆盖内置夹具
自定义夹具还能覆盖已有的,比如让每个测试一打开就跳到 baseURL:
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ baseURL, page }, use) => {
await page.goto(baseURL!);
await use(page);
},
});
第 33 章讲认证复用时提过:也可以覆盖 storageState 夹具,直接注入自己的登录态,不用每次走登录流程。
Tip覆盖要克制。覆盖内置夹具影响面大,只在确有全局统一需求时才动,比如「每个测试都要先登录」。
worker 级夹具
有些资源建一次就够,比如一个测试账号。用 { scope: 'worker' } 让它在一个 worker(工作进程)里只建一次:
import { test as base } from '@playwright/test';
type Account = { username: string; password: string };
// 第一个泛型参数是测试级夹具,第二个才是 worker 级
export const test = base.extend<{}, { account: Account }>({
account: [async ({ browser }, use, workerInfo) => {
const username = `user-${workerInfo.workerIndex}`; // 每个 worker 唯一
// 创建账号、登录...
await use({ username, password: 'verysecure' });
}, { scope: 'worker' }],
});
Warningworker 级夹具的清理要写在
use()之后。它在整个 worker 结束时才拆,别指望它在每条测试后回收。
自动夹具
加 auto: true,夹具对每条测试自动生效,无需声明。比如给失败测试自动存日志:
import { test as base } from '@playwright/test';
export const test = base.extend<{ saveLogs: void }>({
saveLogs: [async ({}, use, testInfo) => {
const logs: string[] = [];
// 收集日志...
await use();
if (testInfo.status !== testInfo.expectedStatus) {
// 测试失败时落盘
await testInfo.attach('logs', { body: logs.join('\n') });
}
}, { auto: true }],
});
选项型夹具做参数化
夹具还能当「配置项」用。声明成 option: true,就能在配置或测试里覆盖它的值,实现同一套测试跑在不同参数下。先在夹具文件里定义默认值:
// my-test.ts
import { test as base } from '@playwright/test';
export type MyOptions = { defaultItem: string };
export const test = base.extend<MyOptions>({
defaultItem: ['Something nice', { option: true }], // 带默认值的可选项
});
再在配置文件里按项目给不同的值:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
import type { MyOptions } from './my-test';
export default defineConfig<MyOptions>({
projects: [
{ name: '购物', use: { defaultItem: 'Buy milk' } },
{ name: '健康', use: { defaultItem: 'Exercise!' } },
],
});
这种「选项夹具」是项目级参数化的底层机制,配合 projects 特别好用。第 42 章讲参数化时还会用到它。
什么时候该抽夹具
我的经验:同一段准备代码在三个以上测试里出现,就值得抽成夹具;如果还要跨文件用,就放进公共 fixtures 文件。第 40 章我们会讲更大的视角——用全局 setup/teardown 处理「整套测试之前之后」的事。
小结
自定义夹具用 test.extend() 造,核心就是接收前一个夹具、调一下 use()、交出准备好的东西。实用技巧几个:夹具依赖别的夹具能自动排顺序,scope: 'worker' 让重资源只建一次,auto: true 省略声明步骤,option: true 能做参数化。抽夹具的时机很简单——同一段准备代码出现三次,就该抽了。