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

Playwright多并行测试中用户数据同步分配方案咨询

解决Playwright并行测试中MFA用户资源竞争问题

针对你遇到的多worker同时抢占同一MFA测试用户导致OTP失效的问题,以下是几种实用的实现方案,核心思路是实现用户的独占式分配与释放,确保同一时间一个用户仅被一个测试用例使用。

方案一:基于Worker索引绑定固定用户(简单无锁)

适合测试用户数量≥并行worker数量的场景,每个worker固定使用一个专属用户,从根源避免竞争。

实现步骤

  1. 准备包含多用户的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"}
]
  1. 在测试文件中通过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数,或需要测试完成后释放用户供其他用例使用,可通过文件锁+状态标记实现动态分配。

实现步骤

  1. 编写用户池管理工具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 };
  1. 在测试文件中调用管理器分配/释放用户:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 17:01:04