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

基于Playwright+Cucumber的多用户并行登录storageState方案问询

问题:基于Cucumber-Playwright用storageState实现多用户并行登录

我想借助cucumber-playwright仓库,在现有测试用例中添加步骤,通过storageState JSON分别以管理员、团队领导、普通用户身份登录,避免重复输入登录信息。需求是:

  • 若目标用户的JSON文件不存在,通过常规登录并存储JSON文件
  • 若文件存在则直接使用
  • 若JSON失效则重建

我已经编写了以下代码,想知道:

  1. 当前方案是否正确?
  2. 后续要并行运行三种用户的登录,是否需要在Before钩子中定义?如果需要该怎么实现?

管理员登录步骤代码

/* eslint-disable indent */
/* eslint-disable prettier/prettier */
// eslint-disable-next-line import/no-unresolved
import { ICustomWorld } from '../support/custom-world';
import { Given } from '@cucumber/cucumber';
import { existsSync, readFileSync } from 'fs';

Given('the user logged in as an admin', async function (this: ICustomWorld) {
  const url = 'https://practice.automationtesting.in/my-account/';
  const authFile = 'playwright/.auth/admin.json';
  let storageState;

  // Check if the JSON file exists and is not empty
  if (existsSync(authFile)) {
    try {
      const fileContent = readFileSync(authFile, 'utf-8').trim();
      if (fileContent !== '') {
        storageState = JSON.parse(fileContent);
      }
    } catch (error) {
      console.error('Error reading or parsing the JSON file:', error);
    }
  }

  if (storageState) {
    // Restore state
    await this.setStorageState(authFile);
  } else {
    // Sign in
    await this.page?.goto(url);
    await this.loginPage?.login('xxxx@xxx.com', 'Test1234567');
    // Save the StorageState
    await this.saveStorageState(authFile);
    await this.page?.waitForLoadState('load');
  }
});

CustomWorld中的saveStorageState和setStorageState方法

async saveStorageState(path: string) {
  if (this.context) {
    const storage = await this.context.storageState();
    await fs.writeFile(path, JSON.stringify(storage));
  }
}

async setStorageState(path: string) {
  if (this.context) {
    const storageState = JSON.parse(await fs.readFile(path, 'utf-8'));
    this.context = await this.browser.newContext({
      storageState, // Use the storageState here
      acceptDownloads: true,
      recordVideo: process.env.PWVIDEO ? { dir: 'screenshots' } : undefined,
      viewport: { width: 1200, height: 800 },
    });
  }
}

Common-hooks的Before钩子代码

Before(async function (this: ICustomWorld, { pickle }: ITestCaseHookParameter) {
  this.startTime = new Date();
  this.testName = pickle.name.replace(/\W/g, '-');
  const storageState = JSON.parse(await fs.readFile('playwright/.auth/admin.json', 'utf-8'));
  this.context = await browser.newContext({
    storageState, // Use the storageState here
    acceptDownloads: true,
    recordVideo: process.env.PWVIDEO ? { dir: 'screenshots' } : undefined,
    viewport: { width: 1200, height: 800 },
  });
});

回答

当前方案的问题

  • Before钩子硬编码管理员身份:现有钩子直接读取管理员auth文件创建context,会导致所有测试默认使用管理员身份,和多用户切换需求冲突,还会覆盖步骤中创建的context。
  • setStorageState方法存在风险:在已有context的情况下重新创建新context,会导致之前的page实例失效,未做清理操作可能引发异常。
  • 未处理JSON失效场景:仅检查文件存在性和非空,未验证storageState是否有效(如cookie过期),无法自动重建失效的认证文件。

多用户并行登录实现方案

不需要在Before钩子中绑定特定用户,而是通过参数化Cucumber步骤动态指定用户,同时保证每个测试场景拥有独立的context,天然支持并行运行:

1. 重构步骤定义,支持多用户参数化

把硬编码的管理员改为通用参数化步骤,集中管理所有用户配置:

/* eslint-disable indent */
/* eslint-disable prettier/prettier */
// eslint-disable-next-line import/no-unresolved
import { ICustomWorld } from '../support/custom-world';
import { Given } from '@cucumber/cucumber';
import { existsSync, readFileSync } from 'fs';

// 集中管理用户账号与对应认证文件路径
const userConfigs = {
  admin: {
    email: 'xxxx@xxx.com',
    password: 'Test1234567',
    authFile: 'playwright/.auth/admin.json'
  },
  team_lead: {
    email: 'teamlead@xxx.com',
    password: 'Test1234567',
    authFile: 'playwright/.auth/team_lead.json'
  },
  regular_user: {
    email: 'user@xxx.com',
    password: 'Test1234567',
    authFile: 'playwright/.auth/regular_user.json'
  }
};

Given('the user logged in as a {string}', async function (this: ICustomWorld, userType: string) {
  const url = 'https://practice.automationtesting.in/my-account/';
  const config = userConfigs[userType as keyof typeof userConfigs];
  if (!config) throw new Error(`未知用户类型: ${userType}`);

  let storageState;
  const authFile = config.authFile;

  // 检查认证文件是否存在且可解析
  if (existsSync(authFile)) {
    try {
      const fileContent = readFileSync(authFile, 'utf-8').trim();
      if (fileContent !== '') storageState = JSON.parse(fileContent);
    } catch (error) {
      console.error(`读取${userType}认证文件失败:`, error);
    }
  }

  // 清理之前的context和page,避免钩子创建的实例干扰
  if (this.page) await this.page.close();
  if (this.context) await this.context.close();

  if (storageState) {
    // 恢复认证状态
    this.context = await this.browser.newContext({
      storageState,
      acceptDownloads: true,
      recordVideo: process.env.PWVIDEO ? { dir: 'screenshots' } : undefined,
      viewport: { width: 1200, height: 800 },
    });
    this.page = await this.context.newPage();
    
    // 验证状态是否有效:跳转后检查登录状态
    await this.page.goto(url);
    const isLoggedIn = await this.loginPage?.isLoggedIn(); // 需要实现isLoggedIn方法(如检查退出按钮/用户头像)
    if (!isLoggedIn) {
      console.log(`${userType}认证状态失效,重新登录`);
      await this.loginPage?.login(config.email, config.password);
      await this.saveStorageState(authFile);
    }
  } else {
    // 全新登录并保存认证状态
    this.context = await this.browser.newContext({
      acceptDownloads: true,
      recordVideo: process.env.PWVIDEO ? { dir: 'screenshots' } : undefined,
      viewport: { width: 1200, height: 800 },
    });
    this.page = await this.context.newPage();
    await this.page.goto(url);
    await this.loginPage?.login(config.email, config.password);
    await this.saveStorageState(authFile);
    await this.page.waitForLoadState('load');
  }
});

2. 修改Before钩子,移除硬编码的storageState

让钩子只做通用初始化,不绑定特定用户:

Before(async function (this: ICustomWorld, { pickle }: ITestCaseHookParameter) {
  this.startTime = new Date();
  this.testName = pickle.name.replace(/\W/g, '-');
  // 创建基础未登录context,后续由步骤动态替换
  this.context = await browser.newContext({
    acceptDownloads: true,
    recordVideo: process.env.PWVIDEO ? { dir: 'screenshots' } : undefined,
    viewport: { width: 1200, height: 800 },
  });
  this.page = await this.context.newPage();
});

3. 优化CustomWorld的saveStorageState方法

确保存储目录存在,避免文件写入失败:

import fs from 'fs/promises';
import path from 'path';

async saveStorageState(pathStr: string) {
  if (this.context) {
    // 自动创建目录(如果不存在)
    const dir = path.dirname(pathStr);
    await fs.mkdir(dir, { recursive: true });
    const storage = await this.context.storageState();
    await fs.writeFile(pathStr, JSON.stringify(storage, null, 2));
  }
}

4. 编写多用户测试场景示例

Scenario: 管理员访问后台管理页
  Given the user logged in as a "admin"
  When the user navigates to the admin dashboard
  Then the admin dashboard should be displayed

Scenario: 团队领导查看项目列表
  Given the user logged in as a "team_lead"
  When the user navigates to the project list page
  Then the project list should be visible

Scenario: 普通用户查看个人中心
  Given the user logged in as a "regular_user"
  When the user navigates to the profile page
  Then the user profile should be displayed

并行运行注意事项

  • Cucumber-Playwright默认支持场景级并行,每个场景拥有独立的browser context,多用户场景可以并行运行,不会互相干扰。
  • 确保每个用户的auth文件路径唯一,避免文件读写冲突。
  • 若并行时出现文件锁问题,可考虑为每个并行进程生成临时认证文件,或添加文件读写锁逻辑。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.17 14:07:55