关注 霍格沃兹测试学院公众号,回复「资料」, 领取人工智能测试开发技术合集
如果你还在为每个测试用例硬编码数据而头疼,或者每次数据变更都要翻遍几十个测试文件——是时候了解数据驱动测试了。今天,我们聊聊如何用 Playwright 优雅地从 Excel 和 JSON 文件中读取测试数据,让你的测试代码真正实现“一次编写,到处运行”。
先看个反例。假设我们要测试一个登录功能,传统写法可能是:
test('用户登录测试', async ({ page }) => {await page.fill('#username', 'zhangsan');await page.fill('#password', '123456');await page.click('#login-btn');// 断言...});test('管理员登录测试', async ({ page }) => {await page.fill('#username', 'admin');await page.fill('#password', 'admin@123');await page.click('#login-btn');// 断言...});发现问题了吗?每增加一个测试账户,就要复制粘贴一整段代码。当密码策略变化时,你得修改所有相关测试文件。这种维护成本,你懂的。
而数据驱动测试的思想很简单:分离测试逻辑与测试数据。我们的目标是把上面的代码改造成这样:
// 测试逻辑只有一份test('登录功能测试', async ({ page }) => {const testData = getTestData(); // 从外部文件读取for (const data of testData) {await performLogin(page, data);// 断言... }});接下来,我们看看具体怎么实现。
Excel 可能是产品经理和业务人员最喜欢的数据格式。如果你的测试数据需要经常让非技术人员维护,Excel 是个不错的选择。
创建一个 testdata.xlsx 文件,内容如下:
保存到项目目录的 data/ 文件夹下。
Playwright 本身不处理 Excel,我们需要借助社区包:
npm install xlsx# 或者yarn add xlsx创建 utils/excelReader.js:
const XLSX = require('xlsx');const path = require('path');classExcelReader{/** * 读取Excel文件 * @param {string}filePath - Excel文件路径 * @param {string}sheetName - 工作表名称(可选,默认为第一个) * @returns {Array}测试数据数组 */static readTestData(filePath, sheetName = null) {try {// 解析文件路径const absolutePath = path.resolve(__dirname, '..', filePath);// 读取工作簿const workbook = XLSX.readFile(absolutePath);// 获取工作表const sheet = sheetName ? workbook.Sheets[sheetName] : workbook.Sheets[workbook.SheetNames[0]];if (!sheet) {thrownewError(`工作表 ${sheetName || '第一个'} 不存在`); }// 转换为JSONconst jsonData = XLSX.utils.sheet_to_json(sheet);console.log(`成功从 ${filePath} 读取 ${jsonData.length} 条测试数据`);return jsonData; } catch (error) {console.error('读取Excel文件失败:', error.message);throw error; } }/** * 按测试场景筛选数据 * @param {string}filePath - Excel文件路径 * @param {string}scenario - 测试场景名称 */static getDataByScenario(filePath, scenario) {const allData = this.readTestData(filePath);return allData.filter(row => row['测试场景'] === scenario); }}module.exports = ExcelReader;现在,让我们重写登录测试:
const { test, expect } = require('@playwright/test');const ExcelReader = require('../utils/excelReader');test.describe('登录功能数据驱动测试', () => {let testData; test.beforeAll(() => {// 一次性读取所有测试数据 testData = ExcelReader.readTestData('./data/testdata.xlsx');console.log(`本次执行将运行 ${testData.length} 个测试用例`); }); test('数据驱动登录测试', async ({ page }) => {// 遍历每条测试数据for (const data of testData) {// 使用测试场景作为子测试名称await test.step(`测试场景: ${data['测试场景']}`, async () => {console.log(`执行用例: ${data['测试场景']}, 用户名: ${data.username}`);// 导航到登录页await page.goto('https://your-app.com/login');// 使用数据填充表单await page.fill('#username', data.username);await page.fill('#password', data.password);await page.click('#login-btn');// 根据预期结果进行断言if (data.expected_result === '登录成功') {await expect(page).toHaveURL('https://your-app.com/dashboard');await expect(page.locator('.welcome-message')).toContainText(data.username); } elseif (data.expected_result.includes('提示')) {await expect(page.locator('.error-message')).toBeVisible();await expect(page.locator('.error-message')).toContainText(data.expected_result); }// 如果是管理员登录的特殊断言if (data.username === 'admin' && data.expected_result === '跳转管理后台') {await expect(page).toHaveURL('https://your-app.com/admin'); } }); } });});优点:
缺点:
伙伴们,对AI测试、大模型评测、质量保障感兴趣吗?我们建了一个 「Playwright mcp技术学习交流群」,专门用来探讨相关技术、分享资料、互通有无。无论你是正在实践还是好奇探索,都欢迎扫码加入,一起抱团成长!期待与你交流!👇

如果你团队里都是开发人员,或者你更喜欢纯文本的版本控制,JSON 可能是更好的选择。
创建 data/loginTestData.json:
{"login_cases": [ {"test_scenario": "普通用户登录","username": "zhangsan","password": "123456","expected_result": "登录成功","permissions": ["view", "edit"],"metadata": {"priority": "P0","tags": ["smoke", "regression"] } }, {"test_scenario": "管理员登录","username": "admin","password": "admin@123","expected_result": "跳转管理后台","permissions": ["view", "edit", "delete", "admin"],"metadata": {"priority": "P1","tags": ["regression"] } }, {"test_scenario": "密码错误","username": "lisi","password": "wrong_pwd","expected_result": "提示密码错误","metadata": {"priority": "P2","tags": ["negative"] } } ],"environment_config": {"base_url": "https://your-app.com","timeout": 30000 }}创建 utils/jsonReader.js:
const fs = require('fs').promises;const path = require('path');classJsonReader{/** * 读取JSON测试数据 * @param {string}filePath - JSON文件路径 * @returns {Promise<Object>}解析后的JSON对象 */staticasync readTestData(filePath) {try {const absolutePath = path.resolve(__dirname, '..', filePath);const fileContent = await fs.readFile(absolutePath, 'utf-8');const jsonData = JSON.parse(fileContent);console.log(`从 ${filePath} 加载了 ${jsonData.login_cases?.length || 0} 个登录测试用例`);return jsonData; } catch (error) {if (error.code === 'ENOENT') {console.error(`文件不存在: ${filePath}`); } elseif (error instanceofSyntaxError) {console.error(`JSON格式错误: ${error.message}`); }throw error; } }/** * 根据标签过滤测试用例 * @param {string}filePath - JSON文件路径 * @param {string}tag - 标签名称 */staticasync getCasesByTag(filePath, tag) {const data = awaitthis.readTestData(filePath);if (!data.login_cases) return [];return data.login_cases.filter(testCase => testCase.metadata?.tags?.includes(tag) ); }/** * 获取环境配置 * @param {string}filePath - JSON文件路径 */staticasync getConfig(filePath) {const data = awaitthis.readTestData(filePath);return data.environment_config || {}; }}module.exports = JsonReader;const { test, expect } = require('@playwright/test');const JsonReader = require('../utils/jsonReader');test.describe('JSON数据驱动登录测试', () => {let testCases;let config; test.beforeAll(async () => {// 异步读取数据和配置const testData = await JsonReader.readTestData('./data/loginTestData.json'); testCases = testData.login_cases; config = testData.environment_config;console.log(`基础URL: ${config.base_url}, 超时: ${config.timeout}ms`); });// 只运行冒烟测试用例 test('冒烟测试:登录功能', async ({ page }) => {const smokeCases = await JsonReader.getCasesByTag('./data/loginTestData.json', 'smoke');for (const testCase of smokeCases) {await test.step(`冒烟测试 - ${testCase.test_scenario}`, async () => {await page.goto(`${config.base_url}/login`);await page.fill('#username', testCase.username);await page.fill('#password', testCase.password);await page.click('#login-btn');// 使用环境配置中的超时时间await page.waitForTimeout(config.timeout);// 这里可以根据你的实际需求添加断言await expect(page).not.toHaveURL(`${config.base_url}/login`); }); } });// 运行所有测试用例,带详细断言 test('完整登录测试套件', async ({ page }) => {for (const testCase of testCases) {await test.step(testCase.test_scenario, async () => {// 这里可以添加更复杂的测试逻辑console.log(`测试用户权限: ${testCase.permissions?.join(', ') || '无'}`);// 实际测试步骤...await page.goto(`${config.base_url}/login`);// ... 更多测试代码 }); } });});如果你想让测试数据在整个项目范围内可用,可以创建自定义 fixture:
// fixtures/testDataFixture.jsconst { test: baseTest } = require('@playwright/test');const JsonReader = require('../utils/jsonReader');const test = baseTest.extend({testData: async ({}, use) => {// 这里可以读取任何你需要的数据文件const data = await JsonReader.readTestData('./data/loginTestData.json');await use(data); },smokeCases: async ({}, use) => {const cases = await JsonReader.getCasesByTag('./data/loginTestData.json', 'smoke');await use(cases); }});module.exports = { test };然后在测试中直接使用:
const { test } = require('../fixtures/testDataFixture');test('使用fixture的测试', async ({ page, testData, smokeCases }) => {console.log(`总用例数: ${testData.login_cases.length}`);console.log(`冒烟用例数: ${smokeCases.length}`);// ... 测试逻辑});优点:
缺点:
根据我的经验,选择建议如下:
选 Excel 如果:
选 JSON 如果:
混合使用(进阶方案):
// 用Excel作为数据源,但转换为JSON格式存储const excelData = ExcelReader.readTestData('./data/raw/source.xlsx');const jsonData = JSON.stringify(excelData, null, 2);await fs.writeFile('./data/processed/testData.json', jsonData);// 然后在测试中使用JSON版本路径问题:始终使用 path.resolve 处理文件路径,避免不同操作系统下的问题。
数据验证:在读取数据后,添加验证逻辑:
functionvalidateTestData(data) {const requiredFields = ['username', 'password', 'expected_result']; data.forEach((row, index) => { requiredFields.forEach(field => {if (!row[field]) {thrownewError(`第${index + 1}行缺少必要字段: ${field}`); } }); });}性能优化:对于大量测试数据,考虑分批执行:
// 分批执行,每批5个用例const batchSize = 5;for (let i = 0; i < testData.length; i += batchSize) {const batch = testData.slice(i, i + batchSize);// 执行批次...}错误处理:添加详细的错误日志,方便调试:
try {await performTest(data);} catch (error) {console.error(`用例失败: ${data.test_scenario}`, {username: data.username,error: error.message });// 可以继续执行下一个用例,而不是整个测试失败}数据驱动测试不是银弹,但它是提升测试代码可维护性的重要手段。通过将测试数据从代码中分离出来:
无论是选择 Excel 还是 JSON,关键是开始实践。从最简单的登录测试开始,逐步将你的测试套件改造为数据驱动模式。你会发现,当产品经理直接给你一个 Excel 文件说“把这些测试用例都跑一下”时,你的内心会是多么的平静。
最后提醒一点:数据驱动测试虽然好,但不要过度设计。简单的、不会频繁变化的测试数据,直接写在代码里也许更合适。找到适合你项目的平衡点,这才是真正的工程智慧。
欢迎加入霍格沃兹测试开发学社 · 人工智能测试开发训练营(VIP)支持 线上线下
