第七篇:《数据驱动测试:利用Excel/JSON/CSV管理测试数据》

当我们需要测试同一个功能(如登录)覆盖多组输入数据时,最笨的方法是复制粘贴测试方法,修改几个参数。更优雅的做法是数据驱动测试:将测试数据与测试逻辑分离,用一个方法执行多组数据。本文将分别用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装饰器配合文件读取同样高效。

相关推荐
是吗乔治3 小时前
vuetify实现excel表格粘贴效果
前端·vue.js·vue·excel
herinspace19 小时前
如何解决管家婆辉煌零售POS中显示的原价和售价不一致?
网络·人工智能·学习·excel·语音识别·零售
sagima_sdu1 天前
Codex 使用指南(技术向):App、CLI 与工作流接入
linux·运维·语言模型·json
l1t1 天前
duckdb excel插件和rusty_sheet插件在python中的不同表现
开发语言·python·excel
agilearchitect1 天前
Matlab导入Excel表格教程:从基础到进阶全攻略
数据结构·其他·matlab·excel
AC赳赳老秦1 天前
OpenClaw与Excel联动:批量读取/写入数据,生成可视化报表
开发语言·python·excel·产品经理·策略模式·deepseek·openclaw
做cv的小昊1 天前
【TJU】研究生应用统计学课程笔记(4)——第二章 参数估计(2.1 矩估计和极大似然估计、2.2估计量的优良性原则)
人工智能·笔记·考研·数学建模·数据分析·excel·概率论
火星papa1 天前
C# 【通过NPIO读写Excel表】
c#·excel·npoi
葡萄城技术团队1 天前
Excel公式前的“@”符号:是Bug还是黑科技?
科技·bug·excel