如果你还在为每个测试用例硬编码数据而头疼,或者每次数据变更都要翻遍几十个测试文件——是时候了解数据驱动测试了。今天,我们聊聊如何用 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 可能是产品经理和业务人员最喜欢的数据格式。如果你的测试数据需要经常让非技术人员维护,Excel 是个不错的选择。
第一步:准备测试数据
创建一个 testdata.xlsx 文件,内容如下:
| 测试场景 | username | password | expected_result |
| 普通用户登录 | zhangsan | 123456 | 登录成功 |
| 管理员登录 | admin | admin@123 | 跳转管理后台 |
| 密码错误 | lisi | wrong_pwd | 提示密码错误 |
| 用户不存在 | notexists | 123456 | 提示用户不存在 |
保存到项目目录的 data/ 文件夹下。
第二步:安装必要的包
Playwright 本身不处理 Excel,我们需要借助社区包:
npm install xlsx # 或者 yarn add xlsx
第三步:实现 Excel 读取工具
创建 utils/excelReader.js:
const XLSX = require('xlsx'); const path = require('path'); class ExcelReader { /** * 读取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 || '第一个'} 不存在`); } // 转换为JSON const 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'); } }); } }); });
Excel 方案的优缺点
优点:
- 非技术人员也能轻松维护
- 支持复杂的数据格式(合并单元格、公式等)
- 可以用 Excel 的数据验证功能保证数据质量
缺点:
- 需要额外依赖
- 版本控制时二进制文件对比困难
- 读取速度相对较慢
实战二:从 JSON 读取测试数据
如果你团队里都是开发人员,或者你更喜欢纯文本的版本控制,JSON 可能是更好的选择。
第一步:创建 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 } }
第二步:创建 JSON 读取工具
创建 utils/jsonReader.js:
const fs = require('fs').promises; const path = require('path'); class JsonReader { /** * 读取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;
第三步:在测试中使用 JSON 数据
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`); // ... 更多测试代码 }); } }); });
更高级的用法:配合 Playwright Fixtures
如果你想让测试数据在整个项目范围内可用,可以创建自定义 fixture:
// fixtures/testDataFixture.js const { 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}`); // ... 测试逻辑 });
JSON 方案的优缺点
优点:
- 纯文本,版本控制友好
- 无需额外依赖(Node.js 原生支持)
- 结构灵活,支持嵌套数据
- 读取速度快
缺点:
- 非技术人员编辑困难
- 没有 Excel 的数据验证功能
- 容易因格式错误导致解析失败
如何选择?
根据我的经验,选择建议如下:
- 选 Excel 如果:
- 测试数据经常由产品/业务人员提供
- 数据需要复杂的计算或格式
- 已经有现成的 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处理文件路径,避免不同操作系统下的问题。 - 数据验证:在读取数据后,添加验证逻辑:
function validateTestData(data) { const requiredFields = ['username', 'password', 'expected_result']; data.forEach((row, index) => { requiredFields.forEach(field => { if (!row[field]) { throw new Error(`第${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 文件说“把这些测试用例都跑一下”时,你的内心会是多么的平静。
最后提醒一点:数据驱动测试虽然好,但不要过度设计。简单的、不会频繁变化的测试数据,直接写在代码里也许更合适。找到适合你项目的平衡点,这才是真正的工程智慧。