Playwright并行测试旧凭证复用致登录失败,如何为每个测试生成新用户
Playwright并行测试中每个测试独立注册新用户的解决方案
问题背景
执行并行测试时,部分测试因复用旧凭证、混用其他测试的登录凭证导致登录失败。原本在worker作用域的fixture中注册用户,但无法实现每个测试使用独立用户,需修改fixtures.ts实现每个worker上的每个测试都使用独立注册的新用户。
原代码文件
原fixtures.ts
import { test as baseTest, request } from '@playwright/test'; import SignUp from '../pages/SignUp'; import fs from 'fs'; import path from 'path'; export * from '@playwright/test'; export const test = baseTest.extend<{}, { workerStorageState: string }>({ // Use the same storage state for all tests in this worker. storageState: ({ workerStorageState }, use) => use(workerStorageState), // Authenticate once per worker with a worker-scoped fixture. workerStorageState: [ async ({ browser }, use) => { // Use parallelIndex as a unique identifier for each worker. const id = test.info().parallelIndex; const fileName = path.resolve(test.info().project.outputDir, `.auth/${id}.json`); if (fs.existsSync(fileName)) { // Reuse existing authentication state if any. await use(fileName); return; } // Important: make sure we authenticate in a clean environment by unsetting storage state. const context = await request.newContext({ storageState: undefined }); const page = await browser.newPage({ storageState: undefined }); // Create fake data for a user and signUp const signUp = new SignUp(page); // SignUp a new user await signUp.SignUp(); await context.storageState({ path: fileName }); await page.context().storageState({ path: fileName }); await context.dispose(); await page.close(); await use(fileName); }, { scope: 'worker' }, ], });
原playwright.config.ts
// @ts-check import { defineConfig, devices } from '@playwright/test'; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ require('dotenv').config(); /** * @see https://playwright.dev/docs/test-configuration */ module.exports = defineConfig({ testDir: './e2e', /* Run tests in files in parallel */ timeout: 90000, expect: { timeout: 60000, }, fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: process.env.VITE_APP_HOST, video: 'on', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', }, /* Configure projects for major browsers */ projects: [ // Setup project { name: 'chromium', use: { ...devices['Desktop Chrome'], }, }, ], /* Run your local dev server before starting the tests */ // webServer: { // command: 'npm run start', // url: 'http://127.0.0.1:3000', // reuseExistingServer: !process.env.CI, // }, });
原测试文件
// @ts-check import { test } from '../../playwright/fixtures'; import Products from '../../pages/ProductAndServices/Products'; import Quotes from '../../pages/Quotes/Quotes'; import LoginPage from '../../pages/LoginPage'; import SignUp from '../../pages/SignUp'; test.describe('Create new product', async () => { let userData: { email: string; password: string; familyName: string; givenName: string; organizationName: string; phoneNumber: string }; let loginPage: LoginPage; let signUp: SignUp; let products: Products; let quotes: Quotes; test.beforeEach(async ({ page }) => { loginPage = new LoginPage(page); signUp = new SignUp(page); products = new Products(page); quotes = new Quotes(page); userData = signUp.userData(); await loginPage.Login(userData.email, userData.password); }); test('Create product from Product & Services', async () => { await products.OpenProductsPage(); await products.OpenNewProduct(); await products.EnterProductName(); await products.EnterProductDescription(); await products.CreateNewCategory(); await products.SelctNewCategory(); await products.AddPurchacePrice(); await products.AddSellingPrice(); await products.SaveProduct(); await products.CheckCreatedProduct(); }); });
修改方案
要实现每个测试用独立用户,需将fixture作用域切换为test级,取消状态文件复用逻辑,确保每个测试生成全新用户并隔离会话状态。
修改后的fixtures.ts
import { test as baseTest } from '@playwright/test'; import SignUp from '../pages/SignUp'; import fs from 'fs'; import path from 'path'; export * from '@playwright/test'; export const test = baseTest.extend<{ authenticatedPage: import('@playwright/test').Page }, {}>({ // 为每个测试创建独立的已登录页面 authenticatedPage: async ({ browser }, use) => { // 生成全局唯一ID:worker索引+测试ID,避免不同测试/worker的状态冲突 const testId = test.info().testId; const workerId = test.info().parallelIndex; const uniqueId = `${workerId}-${testId}`; const fileName = path.resolve(test.info().project.outputDir, `.auth/${uniqueId}.json`); // 创建全新浏览器上下文,彻底隔离会话状态 const context = await browser.newContext({ storageState: undefined }); const page = await context.newPage(); // 注册新用户并完成登录 const signUp = new SignUp(page); await signUp.SignUp(); // 可选:保存会话状态用于调试 await context.storageState({ path: fileName }); // 将已登录页面传递给测试用例 await use(page); // 测试结束后清理上下文 await context.dispose(); // 可选:删除临时状态文件,避免占用存储空间 if (fs.existsSync(fileName)) { fs.unlinkSync(fileName); } }, });
测试文件调整
无需在beforeEach中手动登录,直接使用fixture提供的authenticatedPage:
// @ts-check import { test } from '../../playwright/fixtures'; import Products from '../../pages/ProductAndServices/Products'; import Quotes from '../../pages/Quotes/Quotes'; test.describe('Create new product', () => { let products: Products; let quotes: Quotes; test.beforeEach(async ({ authenticatedPage }) => { products = new Products(authenticatedPage); quotes = new Quotes(authenticatedPage); }); test('Create product from Product & Services', async ({ authenticatedPage }) => { await products.OpenProductsPage(); await products.OpenNewProduct(); await products.EnterProductName(); await products.EnterProductDescription(); await products.CreateNewCategory(); await products.SelctNewCategory(); await products.AddPurchacePrice(); await products.AddSellingPrice(); await products.SaveProduct(); await products.CheckCreatedProduct(); }); });
关键修改说明
- 作用域切换:移除
worker作用域配置,fixture默认以test为作用域,每个测试都会触发一次用户注册流程。 - 唯一标识:用
workerId+testId生成唯一文件名,彻底避免不同测试间的状态文件冲突。 - 上下文隔离:每个测试创建全新浏览器上下文,完全隔离会话状态,杜绝凭证复用问题。
- 自动清理:测试结束后销毁上下文,可选删除临时状态文件,避免资源残留。
- 简化测试逻辑:测试用例直接使用已登录页面,无需重复编写登录代码。
内容的提问来源于stack exchange,提问作者Hamid Heravi
相关产品推荐
相关产品推荐

