当我们需要测试同一个功能(如登录)覆盖多组输入数据时,最笨的方法是复制粘贴测试方法,修改几个参数。更优雅的做法是数据驱动测试:将测试数据与测试逻辑分离,用一个方法执行多组数据。本文将分别用Java(TestNG)和Python(pytest)演示如何从Excel、JSON、CSV读取数据,并给出设计规范与最佳实践。
一、什么是数据驱动测试?
数据驱动测试(DDT):使用不同的输入数据多次运行同一个测试用例,并将预期结果与实际情况比较。
典型的应用场景:
登录功能:多组用户名/密码(正确、错误、空、边界值)
表单验证:不同输入组合下的错误提示
搜索功能:多个关键词组合
配置参数测试:不同配置下的页面行为
优点:
减少重复代码,一个测试方法覆盖所有数据组合
数据与逻辑分离,非技术人员也可维护数据文件
增加测试覆盖率而不增加维护成本
二、数据驱动的方式与工具

本文重点讲解外部文件驱动:Excel、JSON、CSV。
三、Java + TestNG:数据驱动实现
TestNG 提供了 @DataProvider 注解,可以返回 Object[][] 数据。我们结合三大数据源构建 DataProvider。
3.1 准备工作
Maven依赖:
xml
<!-- Apache POI for Excel -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.5</version>
</dependency>
<!-- Jackson for JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
</dependency>
<!-- OpenCSV -->
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>5.8</version>
</dependency>
3.2 从Excel读取数据(.xlsx)
Excel文件 testdata/login_data.xlsx 结构:

工具类:ExcelDataProvider.java
java
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ExcelDataProvider {
public static Object[][] getData(String filePath, String sheetName) {
try (InputStream is = ExcelDataProvider.class.getResourceAsStream(filePath);
Workbook workbook = new XSSFWorkbook(is)) {
Sheet sheet = workbook.getSheet(sheetName);
Row headerRow = sheet.getRow(0);
int colCount = headerRow.getLastCellNum();
List<Map<String, String>> dataList = new ArrayList<>();
for (int i = 1; i <= sheet.getLastRowNum(); i++) {
Row row = sheet.getRow(i);
if (row == null) continue;
Map<String, String> rowMap = new HashMap<>();
for (int j = 0; j < colCount; j++) {
Cell cell = row.getCell(j, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
String header = headerRow.getCell(j).getStringCellValue();
String value = getCellValueAsString(cell);
rowMap.put(header, value);
}
dataList.add(rowMap);
}
// 转换为Object[][]
Object[][] data = new Object[dataList.size()][1];
for (int i = 0; i < dataList.size(); i++) {
data[i][0] = dataList.get(i);
}
return data;
} catch (Exception e) {
throw new RuntimeException("读取Excel失败", e);
}
}
private static String getCellValueAsString(Cell cell) {
if (cell == null) return "";
return switch (cell.getCellType()) {
case STRING -> cell.getStringCellValue();
case NUMERIC -> String.valueOf((long) cell.getNumericCellValue());
case BOOLEAN -> String.valueOf(cell.getBooleanCellValue());
default -> "";
};
}
}
测试用例:
java
java
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.util.Map;
public class LoginDDTTest {
@DataProvider(name = "loginExcelData")
public Object[][] excelData() {
return ExcelDataProvider.getData("/testdata/login_data.xlsx", "Sheet1");
}
@Test(dataProvider = "loginExcelData")
public void testLoginWithExcel(Map<String, String> data) {
String username = data.get("username");
String password = data.get("password");
String expected = data.get("expected_result");
String message = data.get("message");
// 使用Page Object执行登录
LoginPage loginPage = new LoginPage(driver);
if ("success".equals(expected)) {
HomePage home = loginPage.loginAs(username, password);
Assert.assertTrue(home.isWelcomeDisplayed(), message);
} else {
loginPage.loginAs(username, password);
String error = loginPage.getErrorMessage();
Assert.assertTrue(error.contains(message), "错误信息不匹配");
}
}
}
3.3 从JSON读取数据
JSON文件 login_data.json:
json
{ "username": "admin", "password": "123456", "expected": "success", "message": "登录成功" }, { "username": "admin", "password": "wrong", "expected": "fail", "message": "密码错误" }
数据提供者:
java
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
public class JsonDataProvider {
public static Object[][] getData(String jsonPath) {
try (InputStream is = JsonDataProvider.class.getResourceAsStream(jsonPath)) {
ObjectMapper mapper = new ObjectMapper();
List<Map<String, String>> list = mapper.readValue(is, List.class);
Object[][] data = new Object[list.size()][1];
for (int i = 0; i < list.size(); i++) {
data[i][0] = list.get(i);
}
return data;
} catch (Exception e) {
throw new RuntimeException("读取JSON失败", e);
}
}
}
使用方法与Excel类似。
3.4 从CSV读取数据
CSV文件 login_data.csv:
csv
username,password,expected,message
admin,123456,success,登录成功
admin,wrong,fail,密码错误
,any,fail,用户名不能为空
使用OpenCSV:
java
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvException;
import java.io.InputStreamReader;
import java.util.List;
public class CsvDataProvider {
public static Object[][] getData(String csvPath) {
try (CSVReader reader = new CSVReader(new InputStreamReader(
CsvDataProvider.class.getResourceAsStream(csvPath)))) {
List<String[]> allRows = reader.readAll();
String[] headers = allRows.get(0);
Object[][] data = new Object[allRows.size() - 1][1];
for (int i = 1; i < allRows.size(); i++) {
String[] row = allRows.get(i);
Map<String, String> rowMap = new HashMap<>();
for (int j = 0; j < headers.length; j++) {
rowMap.put(headers[j], row.length > j ? row[j] : "");
}
data[i-1][0] = rowMap;
}
return data;
} catch (Exception e) {
throw new RuntimeException("读取CSV失败", e);
}
}
}
四、Python + pytest:数据驱动实现
pytest 主要使用 @pytest.mark.parametrize 装饰器,也可借助 pytest-csv、pytest-excel 或手动读取。
4.1 从CSV读取(最轻量)
CSV文件同上。
读取辅助函数:
python
import csv
import pytest
def load_csv_data(file_path):
data = []
with open(file_path, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
data.append(row)
return data
测试用例:
python
import pytest
from pages.login_page import LoginPage
test_data = load_csv_data("testdata/login_data.csv")
@pytest.mark.parametrize("case", test_data, ids=lambda x: x.get("message", ""))
def test_login_with_csv(driver, case):
login_page = LoginPage(driver)
if case["expected"] == "success":
home = login_page.login_as(case["username"], case["password"])
assert home.is_welcome_displayed(), case["message"]
else:
login_page.login_as(case["username"], case["password"])
error = login_page.get_error_message()
assert case["message"] in error
4.2 从JSON读取
python
import json
def load_json_data(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return data # list of dicts
# 使用同上 parametrize
4.3 从Excel读取(openpyxl)
安装:pip install openpyxl
python
import openpyxl
def load_excel_data(file_path, sheet_name):
wb = openpyxl.load_workbook(file_path, data_only=True)
sheet = wb[sheet_name]
headers = [cell.value for cell in sheet[1]]
data = []
for row in sheet.iter_rows(min_row=2, values_only=True):
row_dict = dict(zip(headers, row))
data.append(row_dict)
return data
五、数据文件设计规范
列命名清晰:使用username而非un,便于理解和维护。
包含标识列:可加case_id或description列,便于定位失败数据。
预期结果描述明确:expected_result可以是success/fail,message是断言内容。
处理空值:CSV和Excel中空单元格要统一转化为空字符串或None。
敏感数据脱敏:不要将真实密码提交到代码仓库,可加密或使用环境变量。
六、数据驱动的优缺点总结

七、最佳实践与注意事项
一个数据文件对应一个测试类:按业务模块组织数据。
测试数据独立于环境:不要在数据文件中硬编码URL,从配置读取。
使用数据ID标记:测试报告中显示正在运行哪一组数据,快速定位。
TestNG: @Test(dataProvider = "...", dataProviderClass = ...) 配合 ITestContext 可设置测试名称。
pytest: ids参数可为每条数据生成描述。
定期清理无用数据:避免数据文件膨胀。
八、总结
核心要点:
数据驱动将测试逻辑与测试数据分离,显著降低维护成本。
TestNG的@DataProvider配合外部文件(Excel/JSON/CSV)灵活强大。
pytest的@pytest.mark.parametrize装饰器配合文件读取同样高效。