Playwright forEach循环变量在test块内访问返回undefined问题
Playwright循环生成多用户用例时test块内变量为undefined解决方案
问题复现
使用Playwright开展Web自动化测试时,通过forEach循环遍历经csv-parse解析得到的多用户CSV数据,配置describe块为serial串行模式实现批量用户测试执行,出现以下异常:
forEach遍历生成的user变量可在beforeAll钩子中正常访问取值,账号、密码字段可正常填入登录表单完成登录- 在
test测试用例块内访问user的属性时返回undefined,例如执行await page.locator('#searchByCode').fill(${user.Code});语句时,无法获取到预期的字段值,已确认CSV文件中对应字段存在有效值。
复现代码如下:
import fs from 'fs'; import path from 'path'; import { test, expect, Page } from '@playwright/test'; import { parse } from 'csv-parse/sync'; const users = parse(fs.readFileSync(path.join(__dirname, '../users.csv')), { columns: true, skip_empty_lines: true }); // 全局配置serial模式 test.describe.configure({ mode: 'serial' }); test.use({ viewport: { width: 600, height: 900 } }); // 全局声明共享page变量 let page: Page; users.forEach(user => { console.log(user.Email); test.describe(`Login for user ${user.Email}`, () => { test.beforeAll(async ({ browser }) => { page = await browser.newPage(); await page.goto('https://XXXXXXX.com'); // 此处可正常获取user属性 await page.fill("#logInEmail",`${user.Email}`); await page.fill('#loginInPassword', `${user.Password}`); await Promise.all([ page.waitForNavigation(), page.locator('#loginSignIn').click() ]); }); test.afterAll(async () => { await page.close(); }); test('SearchBusinessByCode', async ({ browser }, testInfo) => { // 此处获取user属性返回undefined await page.locator('#searchByCode').fill(`${user.Code}`); await page.locator('button', { hasText: 'Search' }).click(); }); }); });
根因分析
- 配置作用域错误:
serial模式被配置在全局作用域,而非每个用户独立的describe块下,所有用户的测试用例、钩子共享同一个执行上下文,全局声明的page变量会被多个beforeAll钩子反复覆盖,执行顺序和变量绑定关系混乱。 - 生命周期不匹配:Playwright执行分为「用例收集」和「用例执行」两个完全独立的阶段,
forEach循环会在收集阶段全部执行完成,配合全局共享变量的写法,执行阶段test块内拿到的user变量引用已经被循环覆盖,最终返回undefined;而beforeAll在收集阶段就和当前循环的describe块绑定,固化了当前轮次的user引用,因此可以正常取值。
修复方案
按照以下步骤调整代码即可解决问题:
- 将
serial模式配置移动到每个用户对应的describe块内部,保证每个用户的测试套件独立串行,不与其他用户的用例共享执行上下文 - 删除全局声明的
page变量,改为在每个describe块内部声明块级作用域的page变量,避免多用户执行时变量被覆盖 - 将
forEach循环替换为for...of循环遍历用户列表,从语法层面减少闭包变量引用异常的概率 - 移除
test块中未使用的browser夹具参数,避免不必要的上下文创建
修复后的可运行代码:
import fs from 'fs'; import path from 'path'; import { test, expect, Page } from '@playwright/test'; import { parse } from 'csv-parse/sync'; const users = parse(fs.readFileSync(path.join(__dirname, '../users.csv')), { columns: true, skip_empty_lines: true }); test.use({ viewport: { width: 600, height: 900 } }); // 使用for...of替代forEach遍历用户列表 for (const user of users) { test.describe(`Login for user ${user.Email}`, () => { // 将serial配置绑定到当前用户的describe块,仅作用于当前用户的用例 test.describe.configure({ mode: 'serial' }); // 在describe块内声明page变量,块级作用域不与其他用户的测试套件共享 let page: Page; test.beforeAll(async ({ browser }) => { page = await browser.newPage(); await page.goto('https://XXXXXXX.com'); await page.fill("#logInEmail", user.Email); await page.fill('#loginInPassword', user.Password); await Promise.all([ page.waitForNavigation(), page.locator('#loginSignIn').click() ]); }); test.afterAll(async () => { await page.close(); }); test('SearchBusinessByCode', async () => { // 此时可正常获取user的所有属性 await page.locator('#searchByCode').fill(user.Code); await page.locator('button', { hasText: 'Search' }).click(); // 后续补充业务断言即可 }); }); }
注意事项
- 禁止在全局作用域声明多个测试套件共享的
page/browser等实例,串行执行时前一个套件的afterAll关闭实例后,后一个套件的用例会拿到已失效的实例引用,或者被后续钩子创建的新实例覆盖,导致操作对象不匹配 - 动态循环生成测试用例时,所有测试配置、变量声明都要绑定到对应的
describe块作用域下,避免全局污染 - 循环生成用例优先使用
for...of语法,可规避绝大多数JS闭包带来的变量引用异常问题
内容的提问来源于stack exchange,提问作者NSS
相关产品推荐
相关产品推荐

