Playwright多并行测试中用户数据同步分配方案咨询
解决Playwright并行测试中MFA用户资源竞争问题
针对你遇到的多worker同时抢占同一MFA测试用户导致OTP失效的问题,以下是几种实用的实现方案,核心思路是实现用户的独占式分配与释放,确保同一时间一个用户仅被一个测试用例使用。
方案一:基于Worker索引绑定固定用户(简单无锁)
适合测试用户数量≥并行worker数量的场景,每个worker固定使用一个专属用户,从根源避免竞争。
实现步骤
- 准备包含多用户的JSON配置文件
test-users.json:
[ {"username": "userA", "password": "passA", "mfaSecret": "base32-secret-A"}, {"username": "userB", "password": "passB", "mfaSecret": "base32-secret-B"}, {"username": "userC", "password": "passC", "mfaSecret": "base32-secret-C"} ]
- 在测试文件中通过
test.info().workerIndex获取当前worker索引,绑定对应用户:
const testUsers = require('./test-users.json'); test('MFA登录验证', async ({ page }) => { // 根据worker索引分配专属用户 const user = testUsers[test.info().workerIndex]; if (!user) throw new Error('测试用户数量不足,无法分配给当前worker'); // 执行登录流程 await page.goto('/login'); await page.locator('#username').fill(user.username); await page.locator('#password').fill(user.password); await page.locator('#submit-password').click(); // 生成并输入TOTP验证码 const otp = require('speakeasy').totp({ secret: user.mfaSecret, encoding: 'base32' }); await page.locator('#otp').fill(otp); await page.locator('#submit-otp').click(); // 验证登录成功 await expect(page.locator('#welcome-msg')).toContainText(`欢迎${user.username}`); });
方案二:动态用户池+文件锁(支持用户复用)
如果测试用户数量少于并行worker数,或需要测试完成后释放用户供其他用例使用,可通过文件锁+状态标记实现动态分配。
实现步骤
- 编写用户池管理工具
user-pool-manager.js:
const fs = require('fs').promises; const path = require('path'); const lockfile = require('proper-lockfile'); // 需安装:npm i proper-lockfile const USER_POOL_PATH = path.join(__dirname, 'user-pool.json'); // 初始化用户池(首次运行时生成带可用状态的用户列表) async function initPool() { try { await fs.access(USER_POOL_PATH); } catch { const rawUsers = require('./test-users.json'); const pool = rawUsers.map(u => ({ ...u, isAvailable: true })); await fs.writeFile(USER_POOL_PATH, JSON.stringify(pool, null, 2)); } } // 分配一个可用用户 async function assignUser() { await initPool(); const releaseLock = await lockfile.lock(USER_POOL_PATH); try { const poolData = await fs.readFile(USER_POOL_PATH, 'utf8'); const pool = JSON.parse(poolData); const availableUser = pool.find(u => u.isAvailable); if (!availableUser) throw new Error('无可用测试用户'); // 标记用户为已占用 availableUser.isAvailable = false; await fs.writeFile(USER_POOL_PATH, JSON.stringify(pool, null, 2)); return availableUser; } finally { await releaseLock(); } } // 释放已使用的用户 async function releaseUser(username) { const releaseLock = await lockfile.lock(USER_POOL_PATH); try { const poolData = await fs.readFile(USER_POOL_PATH, 'utf8'); const pool = JSON.parse(poolData); const targetUser = pool.find(u => u.username === username); if (targetUser) targetUser.isAvailable = true; await fs.writeFile(USER_POOL_PATH, JSON.stringify(pool, null, 2)); } finally { await releaseLock(); } } module.exports = { assignUser, releaseUser };
- 在测试文件中调用管理器分配/释放用户:
const { assignUser, releaseUser } = require('./user-pool-manager'); test('MFA登录验证', async ({ page }) => { let currentUser; try { currentUser = await assignUser(); // 执行登录流程(同方案一) await page.goto('/login'); await page.locator('#username').fill(currentUser.username); await page.locator('#password').fill(currentUser.password); await page.locator('#submit-password').click(); const otp = require('speakeasy').totp({ secret: currentUser.mfaSecret, encoding: 'base32' }); await page.locator('#otp').fill(otp); await page.locator('#submit-otp').click(); await expect(page.locator('#welcome-msg')).toContainText(`欢迎${currentUser.username}`); } finally { // 无论测试成功失败,都释放用户 if (currentUser) await releaseUser(currentUser.username); } });
方案三:上下文扩展封装(优雅复用)
通过Playwright的test.extend扩展测试上下文,自动完成用户的分配与释放,简化测试代码:
const { test: base } = require('@playwright/test'); const { assignUser, releaseUser } = require('./user-pool-manager'); // 扩展测试上下文,添加自动分配的user对象 const test = base.extend({ user: async ({}, use) => { const user = await assignUser(); try { await use(user); } finally { await releaseUser(user.username); } } }); // 测试用例直接使用上下文里的user test('MFA登录验证', async ({ page, user }) => { await page.goto('/login'); await page.locator('#username').fill(user.username); await page.locator('#password').fill(user.password); await page.locator('#submit-password').click(); const otp = require('speakeasy').totp({ secret: user.mfaSecret, encoding: 'base32' }); await page.locator('#otp').fill(otp); await page.locator('#submit-otp').click(); await expect(page.locator('#welcome-msg')).toContainText(`欢迎${user.username}`); });
注意事项
- 确保所有测试用户的MFA采用**TOTP(基于时间)**验证,且每个用户的secret独立,避免验证码冲突。
- 若使用方案二/三,建议在全局setup中重置用户池状态,避免上次测试的残留状态影响。
- CI环境中需确保文件系统支持文件锁,若不支持可改用Redis等内存数据库管理用户池。
内容的提问来源于stack exchange,提问作者Sushant Gupta
相关产品推荐
相关产品推荐

