4. pytest 数据驱动
4.1 参数化介绍
在一些测试场景中,需要输入的测试数据比较多,但流程却是一样的,例如测试登录场景、注册等。针对这种场景,我们可以使用参数化功能来完成相应的测试,即数据驱动测试。
在这种场景中,测试数据和测试用例是多对一的关系,因此可以将它们分开来看,即把数据抽象成参数,通过参数赋值来驱动执行测试,这种被称之为 参数化。通过参数化功能要,可以实现测试数据与测试用例分离,在pytest中可以使用 @pytest.mark.parameterize 。
4.2 参数化应用
通过 @pytest.mark.parameterize 可以实现数据驱动测试,其作用是在收集测试用例的过程中,通过对指定参数的赋值新增被标记对象的调用。
4.2.1 单参数化应用
单参数化应用场景通常为测试方法中仅有一个数据是变化的,即通过一个参数把多组测试数据传递给测试用例。示例代码如下所示:
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_01.py
# @Time 2025-03-13 22:07
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
import pytest
@pytest.mark.parametrize("test_data",[1,(2,3),"Surpass","Shanghai"])
def test_parameterize_01(test_data):
print(f"\n获取到的数据为:{test_data},数据类型为:{type(test_data)}")
代码运行结果如下所示:
============================= test session starts =============================
collecting ... collected 4 items
test_parameterize_01.py::test_parameterize_01[1] PASSED [ 25%]
获取到的数据为:1,数据类型为:<class 'int'>
test_parameterize_01.py::test_parameterize_01[test_data1] PASSED [ 50%]
获取到的数据为:(2, 3),数据类型为:<class 'tuple'>
test_parameterize_01.py::test_parameterize_01[Surpass] PASSED [ 75%]
获取到的数据为:Surpass,数据类型为:<class 'str'>
test_parameterize_01.py::test_parameterize_01[Shanghai] PASSED [100%]
获取到的数据为:Shanghai,数据类型为:<class 'str'>
============================== 4 passed in 0.03s ==============================
4.2.2 多参数化应用
测试数据可以是表达式,输入参数也可以是多个。如果存在多个数据的话,可以通过元组方式来组织数据。示例代码如下所示:
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_02.py
# @Time 2025-03-13 22:12
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
import pytest
@pytest.mark.parametrize("expression,expect",[("1+2",3),("4*8",32),("10-9",2)])
def test_parameterize_01(expression,expect):
actual=eval(expression)
print(f"\n获取到的数据为:{expression},计算结果为{actual},期望结果为:{expect}")
assert actual == expect
运行结果如下所示:
============================= test session starts =============================
collecting ... collected 3 items
test_parameterize_02.py::test_parameterize_01[1+2-3]
test_parameterize_02.py::test_parameterize_01[4*8-32]
test_parameterize_02.py::test_parameterize_01[10-9-2]
========================= 1 failed, 2 passed in 0.06s =========================
PASSED [ 33%]
获取到的数据为:1+2,计算结果为3,期望结果为:3
PASSED [ 66%]
获取到的数据为:4*8,计算结果为32,期望结果为:32
FAILED [100%]
获取到的数据为:10-9,计算结果为1,期望结果为:2
test_parameterize_02.py:8 (test_parameterize_01[10-9-2])
1 != 2
Expected :2
Actual :1
<Click to see difference>
expression = '10-9', expect = 2
@pytest.mark.parametrize("expression,expect",[("1+2",3),("4*8",32),("10-9",2)])
def test_parameterize_01(expression,expect):
actual=eval(expression)
print(f"\n获取到的数据为:{expression},计算结果为{actual},期望结果为:{expect}")
> assert actual == expect
E assert 1 == 2
test_parameterize_02.py:13: AssertionError
4.2.3 多个参数化
一个测试用例是可以被多个参数化数据标记的,示例代码如下所示:
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_03.py
# @Time 2025-03-13 22:18
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
import pytest
@pytest.mark.parametrize("firtst_data",[1,2,3])
@pytest.mark.parametrize("second_data,expect",[(1,2),(4,6)])
def test_parameterize_01(firtst_data,second_data,expect):
print(f"\n获取数据的数据分别为:{firtst_data} - {second_data} - {expect}")
运行结果如下所示:
============================= test session starts =============================
collecting ... collected 6 items
test_parameterize_03.py::test_parameterize_01[1-2-1] PASSED [ 16%]
获取数据的数据分别为:1 - 1 - 2
test_parameterize_03.py::test_parameterize_01[1-2-2] PASSED [ 33%]
获取数据的数据分别为:2 - 1 - 2
test_parameterize_03.py::test_parameterize_01[1-2-3] PASSED [ 50%]
获取数据的数据分别为:3 - 1 - 2
test_parameterize_03.py::test_parameterize_01[4-6-1] PASSED [ 66%]
获取数据的数据分别为:1 - 4 - 6
test_parameterize_03.py::test_parameterize_01[4-6-2] PASSED [ 83%]
获取数据的数据分别为:2 - 4 - 6
test_parameterize_03.py::test_parameterize_01[4-6-3] PASSED [100%]
获取数据的数据分别为:3 - 4 - 6
============================== 6 passed in 0.02s ==============================
从以上结果可以看出,如果存在多个 @pytest.mark.parameterize 时,输入数据则会是多个parameterize的全组合。
4.3 paramterize 参数
4.3.1 argnames参数
parameterize方法的第一个参数是argnames是一个用逗号分隔的字符串或元组。用以表明指定的参数名。argnames通常是与被标记的测试用例的输入参数对应,但在实际使用时,也会受到一些限制。
4.3.1.1 argnames 与测试方法中参数对应关系
1.测试方法未声明,parameterize中声明
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_04.py
# @Time 2025-03-13 22:35
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
import pytest
@pytest.mark.parametrize(["actual","expect"],[(1,1),("Surpass","Surpass")])
def test_parameterize_01(actual):
assert actual == 1
测试用例运行会出现报错,如下所示:
In test_parameterize_01: function uses no argument 'expect'
2.测试方法单独声明parametrize中参数
import pytest
@pytest.mark.parametrize(["actual","expect"],[(1,1),("Surpass","Surpass")])
def test_parameterize_02(actual,expect=10):
assert actual == expect
测试用例运行会出现报错,如下所示:
function already takes an argument 'expect' with a default value
4.3.1.2 argname 与 fixture 同名
通常在使用 fixture 和 parameterize 时,测试用例中传入参数可以一个使用fixture,一个使用 parameterize ,但当出现同名时,parameterize会覆盖同名fixture的参数,示例代码如下所示:
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_05.py
# @Time 2025-03-13 22:43
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
import pytest
@pytest.fixture()
def get_prefix():
return "Test-Fixture-"
@pytest.mark.parametrize("get_prefix,expect",[("Surpass","Test-Parameterize-Surpass"),("Shanghai","Test-Parameterize-Shanghai")])
def test_parameterize_01(get_prefix,expect):
print(f"\n获取到的数据分别为:{get_prefix} - {expect}")
运行结果如下所示:
============================= test session starts =============================
collecting ... collected 2 items
test_parameterize_05.py::test_parameterize_01[Surpass-Test-Parameterize-Surpass] PASSED [ 50%]
获取到的数据分别为:Surpass - Test-Parameterize-Surpass
test_parameterize_05.py::test_parameterize_01[Shanghai-Test-Parameterize-Shanghai] PASSED [100%]
获取到的数据分别为:Shanghai - Test-Parameterize-Shanghai
============================== 2 passed in 0.01s ==============================
4.3.2 argvalues 参数
parameterize方法的第二个参数是arvalues,类型为一个可迭代对象,表示对argvalues参数的赋值。如果包含多个参数,则argvalues的迭代返回元素必须是可度量的值,即支持len()方法,并且长度与argnames中所声明的参数个数相等,因此argvalues可以是元组、列表、集合等。示例代码如下所示:
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_06.py
# @Time 2025-03-13 22:56
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
import pytest
@pytest.mark.parametrize("actual,expect",[(1,2),[20,21],{3,4}])
def test_parameterize_02(actual,expect):
print(f"\n传入的参数分别为:{actual} - {expect}")
assert actual + 1 == expect
运行结果如下所示:
============================= test session starts =============================
collecting ... collected 3 items
test_parameterize_06.py::test_parameterize_02[1-2] PASSED [ 33%]
传入的参数分别为:1 - 2
test_parameterize_06.py::test_parameterize_02[20-21] PASSED [ 66%]
传入的参数分别为:20 - 21
test_parameterize_06.py::test_parameterize_02[3-4] PASSED [100%]
传入的参数分别为:3 - 4
============================== 3 passed in 0.02s ==============================
4.3.2.1 argvalues 来源于Excel
argvalues 是一个可迭代对象,因此可应用于更复杂的场景中。而在日常测试过程,我们通过这一功能从Excel、数据库中先读取数据,再传递给argvalues,示例代码如下所示:
- CSV数据
actual,expect
Surpass,Surpass
Kevin,Kevin
Ben,Ben
- 测试代码:
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_07.py
# @Time 2025-03-13 23:02
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
import csv
import pytest
def read_csv():
data=[]
with open("parameterize.csv",mode="r",encoding="utf-8") as fo:
csv_data=csv.reader(fo,delimiter=",")
for item in csv_data:
data.append(item)
return data[1:]
@pytest.mark.parametrize(["actual","expect"],read_csv())
def test_parameterize_01(actual,expect):
print(f"\n获取到的数据分别为:{actual} - {expect}")
assert actual == expect
&emsp 运行结果如下所示:
============================= test session starts =============================
collecting ... collected 3 items
test_parameterize_07.py::test_parameterize_01[Surpass-Surpass] PASSED [ 33%]
获取到的数据分别为:Surpass - Surpass
test_parameterize_07.py::test_parameterize_01[Kevin-Kevin] PASSED [ 66%]
获取到的数据分别为:Kevin - Kevin
test_parameterize_07.py::test_parameterize_01[Ben-Ben] PASSED [100%]
获取到的数据分别为:Ben - Ben
============================== 3 passed in 0.02s ==============================
4.3.2.2 argvalues 来源于 pytest.param
示例代码如下所示:
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_08.py
# @Time 2025-03-13 23:17
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
import pytest
@pytest.mark.parametrize(["actual","expect"],[(1,1),(2,2),pytest.param(66,69,marks=pytest.mark.xfail,id="xfail")])
def test_parameterize_01(actual,expect):
print(f"\n获取到的数据分别为:{actual} - {expect}")
assert actual == expect
运行结果如下所示:
============================= test session starts =============================
collecting ... collected 3 items
test_parameterize_08.py::test_parameterize_01[1-1]
test_parameterize_08.py::test_parameterize_01[2-2]
test_parameterize_08.py::test_parameterize_01[xfail]
======================== 2 passed, 1 xfailed in 0.05s =========================
PASSED [ 33%]
获取到的数据分别为:1 - 1
PASSED [ 66%]
获取到的数据分别为:2 - 2
XFAIL [100%]
获取到的数据分别为:66 - 69
actual = 66, expect = 69
@pytest.mark.parametrize(["actual","expect"],[(1,1),(2,2),pytest.param(66,69,marks=pytest.mark.xfail,id="xfail")])
def test_parameterize_01(actual,expect):
print(f"\n获取到的数据分别为:{actual} - {expect}")
> assert actual == expect
E assert 66 == 69
test_parameterize_08.py:12: AssertionError
Process finished with exit code 0
4.3.3 indirect 参数
indirect 是一个布尔类型的值或argnames的一个子集,其作用是将指定参数的实参通过 request.param 重定向到和参数同名的fixture中,以此满足更为复杂的场景。indirect默认为false,使用parameterize中的数据,为true时使用fixture中的数据。示例代码如下所示:
# @IDE: PyCharm
# @Project: PyCharmProjects
# @File: test_parameterize_09.py
# @Time 2025-03-13 23:27
# @Author: Surpass Lee
# @E-mail: surpassmebyme@gmail.com
from operator import index
import pytest
@pytest.fixture()
def add(request):
return request.param + 1
@pytest.fixture()
def sub(request):
return request.param - 1
@pytest.mark.parametrize("add,sub",[(1,2),(3,4),(5,6)])
def test_parameterize_01(add,sub):
print(f"\n01: indirect=False 获取到的数据分别为:{add} - {sub}")
@pytest.mark.parametrize("add,sub",[(10,20),(30,40),(50,60)],indirect=True)
def test_parameterize_02(add,sub):
print(f"\n02:indirect=True 获取到的数据分别为:{add} - {sub}")
@pytest.mark.parametrize("add,sub",[(100,20),(300,40),(500,60)],indirect=["add"])
def test_parameterize_03(add,sub):
print(f"\n03: indirect = ['add'] 获取到的数据分别为:{add} - {sub}")
运行结果如下所示:
============================= test session starts =============================
collecting ... collected 9 items
test_parameterize_09.py::test_parameterize_01[1-2] PASSED [ 11%]
01: indirect=False 获取到的数据分别为:1 - 2
test_parameterize_09.py::test_parameterize_01[3-4] PASSED [ 22%]
01: indirect=False 获取到的数据分别为:3 - 4
test_parameterize_09.py::test_parameterize_01[5-6] PASSED [ 33%]
01: indirect=False 获取到的数据分别为:5 - 6
test_parameterize_09.py::test_parameterize_02[10-20] PASSED [ 44%]
02:indirect=True 获取到的数据分别为:11 - 19
test_parameterize_09.py::test_parameterize_02[30-40] PASSED [ 55%]
02:indirect=True 获取到的数据分别为:31 - 39
test_parameterize_09.py::test_parameterize_02[50-60] PASSED [ 66%]
02:indirect=True 获取到的数据分别为:51 - 59
test_parameterize_09.py::test_parameterize_03[100-20] PASSED [ 77%]
03: indirect = ['add'] 获取到的数据分别为:101 - 20
test_parameterize_09.py::test_parameterize_03[300-40] PASSED [ 88%]
03: indirect = ['add'] 获取到的数据分别为:301 - 40
test_parameterize_09.py::test_parameterize_03[500-60] PASSED [100%]
03: indirect = ['add'] 获取到的数据分别为:501 - 60
============================== 9 passed in 0.03s ==============================