前言
实际场景中,我们测试简单注册功能,需要用户名、密码,用户名/密码可能又一些规则,这样需要多种不同规则的数据来验证注册功能。当然我们可以写多个case
,请求一样只是请求数据不同。但是这有一个问题,会造成大量的重复代码,而且不易管理。那该如何优雅解决呢?当然是参数化,那pytest
是如何进行参数化的呢?带着疑问一起探索。
pytest
参数化简介
参数化测试是指在测试用例中通过传入不同的参数来运行多次测试,以验证被测函数或方法的不同输入输出。pytest
参数化使得我们可以方便地对测试用例进行扩展,减少了冗余代码,提高了测试的效率。
pytest参数化的使用方法
使用方法还是很简单的,我们先看一个案例,说不定你一看就懂了。
@pytest.mark.parametrize("input, expected", [
(2, 4),
(3, 9),
(4, 16)
])
def test_square(input, expected):
assert input**2 == expected
以上代码定义了一个名为test_square
的测试用例,其中使用了@pytest.mark.parametrize
装饰器进行参数化。参数化的参数列表包含了多组输入和期望输出,每组参数之间使用逗号分隔。
下面我们执行测试,看看结果:
在命令行中执行以下命令运行测试:
============================= test session starts ==============================
collecting ... collected 3 items
test_demo.py::test_square[2-4]
test_demo.py::test_square[3-9]
test_demo.py::test_square[4-16]
============================== 3 passed in 0.13s ===============================
应用场景
使用方法就如同上面的案例那样,很简单,我们着重看一下应用场景。
单一参数化应用
通常使用场景:测试方法中只有一个数据是变化的,也就是通过一个参数把多组测试数据传递进去。执行时,每组数据都执行一遍。
比如,我们为验证码注册,当前有多个手机号需要注册:
import pytest
@pytest.mark.parametrize("mobile", [
16300000000,
16300000001,
16300000002
])
def test_register(mobile):
print(f"当前注册手机号为:{mobile}")
执行结果如下:
PASSED [ 33%]当前注册手机号为:16300000000
PASSED [ 66%]当前注册手机号为:16300000001
PASSED [100%]当前注册手机号为:16300000002
可以看到,一个测试用例,有多条数据就会执行多次。
多参数应用
测试输入的数据可以是表达式,输入的参数可以是多个。多个数据可以通过元组方式组织。
比如,我们测试计算功能,就可以这样来写:
import pytest
@pytest.mark.parametrize("input, expected", [
(2+2, 4),
(10-1, 9),
(4**2, 16)
])
def test_square(input, expected):
print(input)
assert input == expected
这段代码对输入值进行了不同的计算操作,并验证结果是否符合预期。
多个参数化
一个用例是可以使用多个@pytest.mark.parametrize
来标记的。比如下面这样:
import pytest
@pytest.mark.parametrize('input', [1, 2, 3])
@pytest.mark.parametrize("output, expected", [
(4, 5),
(6, 7)
])
def test_square(input, output, expected):
print(f"input:{input},output:{output},expected:{expected}")
result = input + output
assert result == expected
我们使用了两层嵌套的@pytest.mark.parametrize
装饰器。外层的参数化装饰器指定了input
参数的取值范围为[1, 2, 3]
,内层的参数化装饰器指定了output
和expected
参数的每组取值。
参数化与fixture的结合
fixture
也是可以参数化的,在掌握 pytest fixture:优化你的测试代码(二)这篇文章中有做详细介绍,这篇文章中就不做介绍了,不懂得同学可以翻看这篇文章。
pytestmark实现参数化
pytestmark
可以用于在测试模块级别或类级别上应用装饰器。通过使用pytestmark
,我们可以在测试模块中为多个测试函数统一应用参数化。
我们看案例:
import pytest
pytestmark = pytest.mark.parametrize('input', [1, 2, 3])
def test_square(input):
result = input ** 2
assert result == 4
这段代码,我们在模块级别的pytestmark
变量中使用了pytest.mark.parametrize
装饰器,并将input
参数设置为可取值为[1, 2, 3]
的参数化。
这意味着对于测试模块中的每个测试函数,都会应用参数化,并针对[1, 2, 3]
中的每个值执行测试。在test_square
测试函数中,我们直接使用input
作为参数来访问参数化的值,计算了input
的平方,并断言结果为4。
通过使用pytestmark
,我们可以在整个测试模块中轻松地应用参数化,而无需在每个测试函数中重复相同的装饰器。这种方法特别适用于需要对多个测试函数统一应用相同参数化的情况。
需要注意的是,当使用pytestmark
时,它会将参数化应用于整个模块中的所有测试函数。如果你只想对某个特定的测试函数应用参数化,可以将装饰器直接应用于该函数,而不使用pytestmark
变量。
深挖parametrize
我们先看看源码:/_pytest/python.py
def parametrize(
self,
argnames: Union[str, Sequence[str]],
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
indirect: Union[bool, Sequence[str]] = False,
ids: Optional[
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
] = None,
scope: "Optional[_ScopeName]" = None,
*,
_param_mark: Optional[Mark] = None,
) -> None:
argnames
: 参数名称,可以是一个字符串或字符串列表,表示测试函数中的参数名。如果有多个参数,则可以通过列表指定多个名称。argvalues
: 参数值,可以是一个可迭代对象,其中的每个元素都代表一组参数值。每组参数值可以是一个元组、列表或单个对象。indirect
: 是否将参数作为被测函数的间接参数。如果设置为True
或指定某些参数名称,则在执行测试函数时,参数将作为其他 fixture 函数的返回值传入,而不是直接作为参数值。ids
: 测试用例的标识,用于在测试报告中更好地区分不同的参数化测试用例。可以指定一个可迭代对象或一个函数来生成标识。scope
: 参数的作用域,在多个测试函数之间共享参数时使用。可以设置为"function"
(默认)、"class"
、"module"
或"session"
。_param_mark
: 内部参数,用于指定参数标记。一般情况下不需要关注。
通过多次调用 pytest.parametrize
函数,可以为测试函数添加多组不同的参数化,每次调用都会将参数化应用在之前添加的参数化之上。
注意,pytest.parametrize
函数是在收集测试用例阶段进行参数化的,而不是在每次执行测试用例时动态生成参数。
使用钩子函数pytest_generate_tests
结合parametrize
即可以实现动态生成测试用例。
为了避免重复,这里不做具体案例分析了,有兴趣的小伙伴可以参考这篇文章:深入探索 pytest_generate_tests 钩子函数:动态生成测试实例的利器
pytest.param
Pytest作为一个功能强大且易于使用的测试框架,提供了@pytest.mark.parametrize
装饰器来支持参数化测试。而 pytest.param
函数则是进一步增强参数化测试的工具,它允许我们以更灵活的方式定义和控制参数化测试用例。
参数化标识
在参数化测试中,每个测试用例可能包含多组参数,并且可能会产生大量的测试结果。这时,为了更好地理解和调试测试结果,给每个参数化测试用例指定一个易于理解的标识是很有意义的。而 pytest.param
函数的 id
参数就能做到这一点。
例如,在一个乘法测试中,我们可以定义如下的参数化测试用例:
import pytest
@pytest.mark.parametrize("input, expected", [
pytest.param(2, 4, id="case1"),
pytest.param(3, 9, id="case2"),
pytest.param(5, 25, id="case3")
])
def test_multiply(input, expected):
assert input * input == expected
通过为每个参数化测试用例使用 pytest.param
,我们可以为每个测试用例指定一个标识,方便阅读和理解。当测试运行结束后,测试报告中的标识将帮助我们更好地定位问题。
自定义选项
除了参数标识,pytest.param
函数还可以接受额外的参数,例如 marks
参数,用于为单个测试用例应用自定义标记。通过使用 marks
参数,我们可以在参数化测试中灵活地添加各种自定义选项。
例如,假设我们有一个需要跳过的参数化测试用例,我们可以这样定义:
import pytest
@pytest.mark.parametrize("input, expected", [
pytest.param(2, 4, marks=pytest.mark.skip),
pytest.param(3, 9, marks=pytest.mark.skip),
pytest.param(5, 25)
])
def test_multiply(input, expected):
assert input * input == expected
在上述示例中,我们使用 pytest.mark.skip
标记来跳过前两个测试用例。通过这种方式,我们可以对不同的参数化测试用例应用不同的标记,例如 pytest.mark.skip
、pytest.mark.xfail
等,以实现更灵活的测试控制。
最后
通过本文的介绍,我们了解了pytest参数化的概念、使用方法和案例演示。pytest参数化可以极大地简化测试用例的编写,提高代码的复用性和可维护性。在实际的软件测试中,合理利用pytest参数化将帮助我们更高效地进行测试,并更快地发现潜在的问题。另外我们还介绍了parametrize
的高级用法,可以结合钩子函数pytest_generate_tests
实现复杂场景。最后我们介绍了一个非常有用的函数pytest.param
,它为参数化测试提供了更多的自定义选项和控制。因此,在编写参数化测试时,不妨考虑使用 pytest.param
来提升测试的质量和效率。