Playwright测试中如何确保fixture的beforeEach在每个测试中执行
我用Playwright编写E2E测试,定义了两个Fixture:
- 启动带有Chrome扩展的浏览器(fixtures.js)
- Mock外部API调用以测试认证状态(fixtures-auth.js)
两个测试文件分别位于不同目录,均通过import { test, expect } from '../fixtures-auth.js'导入Fixture,但同时运行测试时,仅第一个测试会执行fixtures-auth.js中的test.beforeEach逻辑,第二个测试虽导入却未执行。单独运行每个测试均正常,目标是让每个测试都自动执行该beforeEach以避免代码重复。
相关代码
fixtures-auth.js
import { test } from './fixtures.js'; export { test, expect } from './fixtures.js'; test.beforeEach(async ({ context }) => { await context.route( 'https://some.url.com/api/some-endpoint', async (route) => { if (route.request().serviceWorker()) { const json = { // json content }; await route.fulfill({ json }); } }, ); });
fixtures.js
const base = require('@playwright/test'); const { chromium } = require('@playwright/test'); const path = require('path'); process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = '1'; exports.test = base.test.extend({ context: async ({ }, use) => { const pathToExtension = path.join(__dirname, '../dist'); const context = await chromium.launchPersistentContext('', { headless: false, args: [ `--disable-extensions-except=${pathToExtension}`, `--load-extension=${pathToExtension}`, ], }); await use(context); await context.close(); }, extensionId: async ({ context }, use) => { // for manifest v3: let [background] = context.serviceWorkers(); if (!background) background = await context.waitForEvent('serviceworker'); const extensionId = background.url().split('/')[2]; await use(extensionId); }, }); exports.expect = base.expect;
Playwright 配置文件
const { defineConfig } = require('@playwright/test'); const baseURL = 'http://127.0.0.1:8080'; module.exports = defineConfig({ testDir: './playwright', fullyParallel: true, forbidOnly: false, retries: 0, maxFailures: 1, workers: 1, reporter: 'line', expect: { timeout: 10 * 1000, }, use: { baseURL: baseURL, trace: 'on-first-retry', serviceWorkers: 'allow', }, projects: [ { name: 'chromium', use: { browserName: 'chromium' }, }, ], webServer: { command: 'npx http-server ./playwright', url: baseURL, reuseExistingServer: true, }, });
问题核心在于fixtures-auth.js的写法:直接在从fixtures.js导入的test实例上注册全局beforeEach,虽然模块缓存会让这个注册全局生效,但Playwright的测试隔离机制可能导致后续测试无法触发该钩子;或者因为上下文创建与钩子执行的时序问题,导致路由未正确绑定到新的测试上下文。
另外,你要Mock的是Chrome扩展后台Service Worker的请求,当前写法依赖beforeEach在每个测试开始时给新上下文添加路由,但串行运行时,第二个测试的上下文创建后,钩子可能未被正确触发(或你误以为未触发)。
方案1:将路由逻辑整合到Context Fixture中(推荐)
修改fixtures-auth.js,通过extend扩展Context Fixture,确保每个新创建的上下文都会自动添加路由规则,完全避免依赖全局beforeEach:
import { test as baseTest } from './fixtures.js'; // 扩展原有test实例,为context添加路由逻辑 export const test = baseTest.extend({ context: async ({ context }, use) => { await context.route( 'https://some.url.com/api/some-endpoint', async (route) => { if (route.request().serviceWorker()) { const json = { // json content }; await route.fulfill({ json }); } } ); // 将配置好的上下文传递给测试 await use(context); } }); export { expect } from './fixtures.js';
这种方式利用Playwright Fixture的生命周期,每个测试的上下文在创建后都会立即绑定路由,完全隔离且可靠,无论单独还是批量运行测试都会生效。
方案2:验证并修复全局beforeEach的触发
如果你希望保留原有的beforeEach写法,可以先添加日志验证钩子是否真的未执行:
test.beforeEach(async ({ context }) => { console.log(`[BEFORE EACH] 为上下文添加路由`); await context.route(/* ... */); });
运行测试后查看控制台输出:
- 如果第二个测试有日志输出,说明钩子已执行,问题出在路由匹配逻辑(例如Service Worker请求未触发路由),可以检查
route.request().serviceWorker()的判断是否准确,或调整路由注册时机(确保Service Worker已启动后再注册路由)。 - 如果无日志输出,说明钩子未被注册到第二个测试,可将
fixtures-auth.js改为导出扩展后的test实例:
import { test as baseTest } from './fixtures.js'; export const test = baseTest; test.beforeEach(async ({ context }) => { await context.route(/* ... */); }); export { expect } from './fixtures.js';
额外注意事项
- 你的Playwright配置中
fullyParallel: true但workers: 1,测试会串行执行,每个测试都会创建全新的上下文,因此必须确保每个上下文都独立配置路由。 - 由于你Mock的是Service Worker的请求,确保在路由注册前,Service Worker已完成启动(可利用
fixtures.js中extensionIdFixture的等待逻辑,确保Service Worker就绪后再执行测试逻辑)。
内容的提问来源于stack exchange,提问作者mrzalais

