在软件测试中,**数据驱动测试**是一种重要的方法,可以通过多组测试数据验证功能的正确性,提高测试覆盖率,同时减少冗余的代码。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}"