pytest-数据驱动

在软件测试中,**数据驱动测试**是一种重要的方法,可以通过多组测试数据验证功能的正确性,提高测试覆盖率,同时减少冗余的代码。Pytest 是一个灵活而强大的测试框架,其内置支持数据驱动测试功能,可以进行测试参数化,使得实现和管理数据驱动测试变得轻松高效。

一、什么是数据驱动测试?

数据驱动测试(Data-Driven Testing,DDT)是一种通过参数化实现的测试方法。它的核心思想是将测试逻辑与测试数据分离。测试逻辑只需实现一次,通过多组不同的数据进行重复执行,从而验证代码在不同场景下的表现。

数据驱动的优势

  • 代码复用性高:一个测试函数可覆盖多组测试数据。
  • 可维护性强:测试逻辑和测试数据分离,数据的更新不会影响测试逻辑。
  • 易扩展:新增测试场景时,只需添加测试数据,无需修改测试逻辑。

这里主要介绍通过pytest的参数化实现数据驱动测试

二、pytest从参数化到数据驱动

2.1、pytest参数化

pytest参数化就是用一组数据驱动同一个测试逻辑,它允许你使用不同的输入数据多次运行同一个测试函数,从而避免编写重复的测试代码。主要的参数化方式如下:

  • pytest.fixture()使用fixture传params参数实现参数化
  • @pytest.mark.parametrize允许在测试函数或类中定义基础参数
  • @pytest.mark.parametrize+fixture实现间接参数化
  • 钩子函数pytest_generate_tests

2.1.1、pytest.fixture()使用fixture传params参数

fixture实现的参数化可以给不同的测试函数使用,适用于多个测试函数共享同一组"基础数据"或"环境配置"的场景。

python 复制代码
import pytest

@pytest.fixture(params=["chrome", "firefox", "safari"])# 定义不同的测试数据
def browser(request):
    """每个参数都会创建一个fixture实例"""
    browser_name = request.param #获取fixture的参数值
    print(f"\n初始化浏览器: {browser_name}")
    yield browser_name
    print(f"关闭浏览器: {browser_name}")

def test_with_browser_fixture(browser):
    """每个浏览器参数都会运行一次这个测试"""
    assert browser in ["chrome", "firefox", "safari"]

@pytest.fixture(params=[(1, 2), (3, 4), (5, 6)])
def number_pair(request):
    return request.param

def test_addition_with_fixture(number_pair):
    a, b = number_pair
    assert a + b == a + b  # 实际测试逻辑

2.1.2、@pytest.mark.parametrize允许在测试函数或类中定义基础参数

为单个测试函数提供多组输入参数,执行相同的测试逻辑,直接在测试函数上当定义的装饰器,适用于同一个功能点的边界测试

python 复制代码
# 1、一个参数一个值
@pytest.mark.parametrize("input", ["输入值"])

#2、一个参数多个值
@pytest.mark.parametrize("input", ["输入值1", "输入值2", "输入值3", "输入值4", "输入值5"])

# 3、多个参数多个值
@pytest.mark.parametrize("userName,passWord",[("xiaqiang", "123456"), ("rose", "123456"), ("jone", "123456"), ("Alix", "123456")])

# 4、多个参数混个使用
data1 = [1, 2]
data2 = ["python", "java"]
data3 = ["暴", "躁", "测", "试", "君"]
@pytest.mark.parametrize("a", data1)
@pytest.mark.parametrize("b", data2)
@pytest.mark.parametrize("c", data3)

# 5、参数化,传入字典数据
json=({"username":"alex","password":"123456"},{"username":"rongrong","password":"123456"})
@pytest.mark.parametrize('json', json)

# 6、参数化集合标记的使用
@pytest.mark.parametrize("user,pwd",[("xiaoqiang", "123456"), ("rose", "123456"),pytest.param("jone", "123456", marks=pytest.mark.xfail),pytest.param("Alex", "123456", marks=pytest.mark.skip)])

2.1.3、@pytest.mark.parametrize+fixture组合

这两种方式可以结合使用,产生测试参数的交集(笛卡尔积),这在你需要测试多种数据组合多种配置时非常强大。比如测试不同浏览器上的登录操作,用于测试环境的初始化和清理

python 复制代码
import pytest

@pytest.fixture(params=["chrome", "firefox"]) # 参数1:浏览器类型
def browser(request):
    return request.param

@pytest.mark.parametrize("username, password", [
    ("user1", "pass1"),
    ("admin", "secret"),
]) # 参数2:用户名密码组合
def test_login(browser, username, password):
    """这个测试会运行 2 (browsers) x 2 (user/pass) = 4 次"""
    print(f"Testing login on {browser} with user: {username}")
    #模拟登录逻辑
    assert True

比如在fixture中设置初始化

python 复制代码
import pytest

@pytest.fixture
def setup_database():
    """设置测试数据库"""
    print("\n设置数据库...")
    db_connection = {"status": "connected", "data": []}
    yield db_connection
    print("清理数据库...")

@pytest.mark.parametrize("user_id,expected_name", [
    (1, "Alice"),
    (2, "Bob"),
    (3, "Charlie")
])
def test_user_query(setup_database, user_id, expected_name):
    """测试用户查询功能"""
    db = setup_database
    print(f"查询用户ID: {user_id}, 期望姓名: {expected_name}")
    # 模拟数据库查询
    assert db["status"] == "connected"

2.1.4、钩子函数pytest_generate_tests

pytest_generate_tests可以在收集用例阶段,将测试数据参数化。

python 复制代码
# conftest.py

def pytest_generate_tests(metafunc):
    #metafunc.fixturenames为param
    if "param" in metafunc.fixturenames: 
        # 参数化测试用例,param后面的参数为参数化入参的值与pytest.mark.parametrize用法一致
        metafunc.parametrize("param", metafunc.module.case_data,ids=metafunc.module.case_id, scope="function")

# test_demo_gen.py

case_id = ["001", "002"]

case_data = [{"url": "https://www.baidu.com/"},
             {"url": "https://www.cnblogs.com/alltests/"}]

#测试用例引用的参数需要与参数化测试用的parm一致,否则无法参数化成功
def test_url(param):
    print("\n参数:" + str(param))

pytest -vs test_demo_gen.py的执行结果为:
testcase/test_demo_g.py::test_url[001]
参数:{'url': 'https://www.baidu.com/'}
PASSED
testcase/test_demo_g.py::test_url[002]
参数:{'url': 'https://www.cnblogs.com/alltests/'}
PASSED

2.2、pytest数据驱动

数据驱动是一种思想,指将测试数据与测试逻辑分离,通过外部数据源(如列表、字典、JSON、YAML、Excel 等)来驱动测试用例的执行。测试数据来自外部或独立的变量/函数,使得测试逻辑非常清晰

python 复制代码
import pytest

# 将测试数据定义在外部(这里是一个变量,也可以从文件读取)
test_data = [
    {"input": 1, "expected": 2},
    {"input": 2, "expected": 3},
    {"input": 3, "expected": 4},
    {"input": -1, "expected": 0}, # 边界情况
]

# 定义一个函数来获取数据(可以从JSON/YAML/CSV文件读取)
def get_test_data():
    # 这里可以是从文件加载数据的逻辑
    # with open('data.json') as f:
    #     return json.load(f)
    return test_data

# 测试逻辑只关心操作,不关心具体数据
@pytest.mark.parametrize("data", get_test_data())
def test_increment_advanced(data):
    # 通过字典键名获取数据,代码可读性更强
    result = data["input"] + 1
    assert result == data["expected"]

# 或者更优雅地"解包"字典
@pytest.mark.parametrize("input, expected", [
    (item["input"], item["expected"]) for item in get_test_data()
])
def test_increment_advanced_2(input, expected):
    assert input + 1 == expected

优雅的数据驱动实现

yaml 复制代码
# 登录接口测试数据
test_login:
  success:
    name: "成功登录测试"
    request:
      url: "/api/login"
      method: "post"
      headers:
        Content-Type: "application/json"
      json:
        username: "admin"
        password: "admin123"
    validate:
      status_code: 200
      json:
        code: 0
        message: "success"
  
  wrong_password:
    name: "密码错误测试"
    request:
      url: "/api/login"
      method: "post"
      headers:
        Content-Type: "application/json"
      json:
        username: "admin"
        password: "wrong"
    validate:
      status_code: 401
      json:
        code: 1001
        message: "用户名或密码错误"
python 复制代码
#conftest.py
import pytest
import yaml

def pytest_generate_tests(metafunc):
    """动态生成测试用例"""
    if "login_test_data" in metafunc.fixturenames:
        # 从 YAML 文件加载测试数据
        with open('data/test_login.yaml', 'r', encoding='utf-8') as f:
            test_data = yaml.safe_load(f)
        
        # 将测试数据转换为参数化格式
        test_cases = []
        for case_key, case_value in test_data.items():
            test_cases.append(pytest.param(
                case_value,
                id=case_value["name"]  # 在测试报告中显示用例名称
            ))
        
        metafunc.parametrize("login_test_data", test_cases)
python 复制代码
#测试用例test.py
import pytest
import requests

class TestLogin:
    
    def test_login_cases(self, login_test_data, base_url):
        """使用数据驱动的登录测试"""
        
        # 准备请求数据
        request_data = login_test_data["request"]
        validate_data = login_test_data["validate"]
        
        # 发送请求
        url = base_url + request_data["url"]
        response = requests.request(
            method=request_data["method"],
            url=url,
            headers=request_data.get("headers", {}),
            json=request_data.get("json", {})
        )
        
        # 验证响应
        assert response.status_code == validate_data["status_code"]
        response_json = response.json()
        
        # 验证 JSON 响应体
        for key, expected_value in validate_data["json"].items():
            actual_value = response_json.get(key)
            assert actual_value == expected_value, \
                f"字段 {key} 验证失败: 期望 {expected_value}, 实际 {actual_value}"
相关推荐
我的xiaodoujiao19 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 27--二次封装方法--优化断言结果
python·学习·测试工具·pytest
淼_@淼3 天前
pytest简介
运维·服务器·pytest
奶茶精Gaaa4 天前
【ZJ】Pytest框架搭建
pytest
趙卋傑4 天前
接口自动化测试
python·pycharm·pytest
测试界萧萧6 天前
Jenkins+Allure+Pytest的持续集成
自动化测试·软件测试·功能测试·程序人生·ci/cd·jenkins·pytest
nvd116 天前
Pytest 中使用 SQLAlchemy 进行异步数据库测试
数据库·oracle·pytest
文人sec8 天前
pytest1-接口自动化测试场景
软件测试·python·单元测试·pytest
我的xiaodoujiao8 天前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 25--数据驱动--参数化处理 Excel 文件 2
前端·python·学习·测试工具·ui·pytest