基于Playwright+Cucumber的多用户并行登录storageState方案问询
问题:基于Cucumber-Playwright用storageState实现多用户并行登录
我想借助cucumber-playwright仓库,在现有测试用例中添加步骤,通过storageState JSON分别以管理员、团队领导、普通用户身份登录,避免重复输入登录信息。需求是:
- 若目标用户的JSON文件不存在,通过常规登录并存储JSON文件
- 若文件存在则直接使用
- 若JSON失效则重建
我已经编写了以下代码,想知道:
- 当前方案是否正确?
- 后续要并行运行三种用户的登录,是否需要在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
相关产品推荐
相关产品推荐

