当我们需要测试同一个功能(如登录)覆盖多组输入数据时,最笨的方法是复制粘贴测试方法,修改几个参数。更优雅的做法是数据驱动测试:将测试数据与测试逻辑分离,用一个方法执行多组数据。本文将分别用Java(TestNG)和Python(pytest)演示如何从Excel、JSON、CSV读取数据,并给出设计规范与最佳实践。
一、什么是数据驱动测试?
数据驱动测试(DDT):使用不同的输入数据多次运行同一个测试用例,并将预期结果与实际情况比较。
典型的应用场景:
登录功能:多组用户名/密码(正确、错误、空、边界值)
表单验证:不同输入组合下的错误提示
搜索功能:多个关键词组合
配置参数测试:不同配置下的页面行为
优点:
减少重复代码,一个测试方法覆盖所有数据组合
数据与逻辑分离,非技术人员也可维护数据文件
增加测试覆盖率而不增加维护成本
二、数据驱动的方式与工具
本文重点讲解外部文件驱动:Excel、JSON、CSV。
三、Java + TestNG:数据驱动实现
TestNG 提供了 @DataProvider 注解,可以返回 Object[][] 数据。我们结合三大数据源构建 DataProvider。
3.1 准备工作
Maven依赖:
<!-- 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
importorg.apache.poi.ss.usermodel.*;importorg.apache.poi.xssf.usermodel.XSSFWorkbook;importjava.io.InputStream;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Map;publicclassExcelDataProvider{publicstaticObject[][]getData(StringfilePath,StringsheetName){try(InputStreamis=ExcelDataProvider.class.getResourceAsStream(filePath);Workbookworkbook=newXSSFWorkbook(is)){Sheetsheet=workbook.getSheet(sheetName);RowheaderRow=sheet.getRow(0);intcolCount=headerRow.getLastCellNum();List<Map<String,String>>dataList=newArrayList<>();for(inti=1;i<=sheet.getLastRowNum();i++){Rowrow=sheet.getRow(i);if(row==null)continue;Map<String,String>rowMap=newHashMap<>();for(intj=0;j<colCount;j++){Cellcell=row.getCell(j,Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);Stringheader=headerRow.getCell(j).getStringCellValue();Stringvalue=getCellValueAsString(cell);rowMap.put(header,value);}dataList.add(rowMap);}// 转换为Object[][]Object[][]data=newObject[dataList.size()][1];for(inti=0;i<dataList.size();i++){data[i][0]=dataList.get(i);}returndata;}catch(Exceptione){thrownewRuntimeException("读取Excel失败",e);}}privatestaticStringgetCellValueAsString(Cellcell){if(cell==null)return"";returnswitch(cell.getCellType()){caseSTRING->cell.getStringCellValue();caseNUMERIC->String.valueOf((long)cell.getNumericCellValue());caseBOOLEAN->String.valueOf(cell.getBooleanCellValue());default->"";};}}测试用例:
java
importorg.testng.annotations.DataProvider;importorg.testng.annotations.Test;importjava.util.Map;publicclassLoginDDTTest{@DataProvider(name="loginExcelData")publicObject[][]excelData(){returnExcelDataProvider.getData("/testdata/login_data.xlsx","Sheet1");}@Test(dataProvider="loginExcelData")publicvoidtestLoginWithExcel(Map<String,String>data){Stringusername=data.get("username");Stringpassword=data.get("password");Stringexpected=data.get("expected_result");Stringmessage=data.get("message");// 使用Page Object执行登录LoginPageloginPage=newLoginPage(driver);if("success".equals(expected)){HomePagehome=loginPage.loginAs(username,password);Assert.assertTrue(home.isWelcomeDisplayed(),message);}else{loginPage.loginAs(username,password);Stringerror=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”: “密码错误”
}
]
数据提供者:
importcom.fasterxml.jackson.databind.ObjectMapper;importjava.io.InputStream;importjava.util.List;importjava.util.Map;publicclassJsonDataProvider{publicstaticObject[][]getData(StringjsonPath){try(InputStreamis=JsonDataProvider.class.getResourceAsStream(jsonPath)){ObjectMappermapper=newObjectMapper();List<Map<String,String>>list=mapper.readValue(is,List.class);Object[][]data=newObject[list.size()][1];for(inti=0;i<list.size();i++){data[i][0]=list.get(i);}returndata;}catch(Exceptione){thrownewRuntimeException("读取JSON失败",e);}}}使用方法与Excel类似。
3.4 从CSV读取数据
CSV文件 login_data.csv:
csv
username,password,expected,message
admin,123456,success,登录成功
admin,wrong,fail,密码错误
,any,fail,用户名不能为空
使用OpenCSV:
importcom.opencsv.CSVReader;importcom.opencsv.exceptions.CsvException;importjava.io.InputStreamReader;importjava.util.List;publicclassCsvDataProvider{publicstaticObject[][]getData(StringcsvPath){try(CSVReaderreader=newCSVReader(newInputStreamReader(CsvDataProvider.class.getResourceAsStream(csvPath)))){List<String[]>allRows=reader.readAll();String[]headers=allRows.get(0);Object[][]data=newObject[allRows.size()-1][1];for(inti=1;i<allRows.size();i++){String[]row=allRows.get(i);Map<String,String>rowMap=newHashMap<>();for(intj=0;j<headers.length;j++){rowMap.put(headers[j],row.length>j?row[j]:"");}data[i-1][0]=rowMap;}returndata;}catch(Exceptione){thrownewRuntimeException("读取CSV失败",e);}}}四、Python + pytest:数据驱动实现
pytest 主要使用 @pytest.mark.parametrize 装饰器,也可借助 pytest-csv、pytest-excel 或手动读取。
4.1 从CSV读取(最轻量)
CSV文件同上。
读取辅助函数:
importcsvimportpytestdefload_csv_data(file_path):data=[]withopen(file_path,mode='r',encoding='utf-8')asfile:reader=csv.DictReader(file)forrowinreader:data.append(row)returndata测试用例:
importpytestfrompages.login_pageimportLoginPage test_data=load_csv_data("testdata/login_data.csv")@pytest.mark.parametrize("case",test_data,ids=lambdax:x.get("message",""))deftest_login_with_csv(driver,case):login_page=LoginPage(driver)ifcase["expected"]=="success":home=login_page.login_as(case["username"],case["password"])asserthome.is_welcome_displayed(),case["message"]else:login_page.login_as(case["username"],case["password"])error=login_page.get_error_message()assertcase["message"]inerror4.2 从JSON读取
importjsondefload_json_data(file_path):withopen(file_path,'r',encoding='utf-8')asf:data=json.load(f)returndata# list of dicts# 使用同上 parametrize4.3 从Excel读取(openpyxl)
安装:pip install openpyxl
importopenpyxldefload_excel_data(file_path,sheet_name):wb=openpyxl.load_workbook(file_path,data_only=True)sheet=wb[sheet_name]headers=[cell.valueforcellinsheet[1]]data=[]forrowinsheet.iter_rows(min_row=2,values_only=True):row_dict=dict(zip(headers,row))data.append(row_dict)returndata五、数据文件设计规范
列命名清晰:使用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装饰器配合文件读取同样高效。