如果你还在为每个测试用例硬编码数据而头疼,或者每次数据变更都要翻遍几十个测试文件——是时候了解数据驱动测试了。今天,我们聊聊如何用 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 文件,内容如下:
测试场景 | username | password | expected_result |
|---|---|---|---|
普通用户登录 | zhangsan | 123456 | 登录成功 |
管理员登录 | admin | admin@123 | 跳转管理后台 |
密码错误 | lisi | wrong_pwd | 提示密码错误 |
用户不存在 | notexists | 123456 | 提示用户不存在 |
保存到项目目录的 data/ 文件夹下。
Playwright 本身不处理 Excel,我们需要借助社区包:
npm install xlsx
# 或者
yarn add xlsx创建 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');
}
});
}
});
});优点:
缺点:
如果你团队里都是开发人员,或者你更喜欢纯文本的版本控制,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');
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;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.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}`);
// ... 测试逻辑
});优点:
缺点:
根据我的经验,选择建议如下:
path.resolve 处理文件路径,避免不同操作系统下的问题。数据驱动测试不是银弹,但它是提升测试代码可维护性的重要手段。通过将测试数据从代码中分离出来:
无论是选择 Excel 还是 JSON,关键是开始实践。从最简单的登录测试开始,逐步将你的测试套件改造为数据驱动模式。你会发现,当产品经理直接给你一个 Excel 文件说“把这些测试用例都跑一下”时,你的内心会是多么的平静。
最后提醒一点:数据驱动测试虽然好,但不要过度设计。简单的、不会频繁变化的测试数据,直接写在代码里也许更合适。找到适合你项目的平衡点,这才是真正的工程智慧。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。