You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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();
  });
});

关键修改说明

  1. 作用域切换:移除worker作用域配置,fixture默认以test为作用域,每个测试都会触发一次用户注册流程。
  2. 唯一标识:用workerId+testId生成唯一文件名,彻底避免不同测试间的状态文件冲突。
  3. 上下文隔离:每个测试创建全新浏览器上下文,完全隔离会话状态,杜绝凭证复用问题。
  4. 自动清理:测试结束后销毁上下文,可选删除临时状态文件,避免资源残留。
  5. 简化测试逻辑:测试用例直接使用已登录页面,无需重复编写登录代码。

内容的提问来源于stack exchange,提问作者Hamid Heravi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 09:48:07