pytest参数化测试¶
发布于:2021-06-06 | 分类:devops , python/vba/cpp
pytest
是一个流行的Python单元测试框架,自身功能强大,且可以集成各种插件例如代码覆盖率、测试报告。我们可能遇到的这样的应用场景,同一功能需要在大量数据集上进行测试,并且每一个测试样本对应一个测试案例。显然,每个测试案例仅是输入不同,为每一个测试样本写一个测试函数不太现实。本文介绍的pytest参数化测试即是针对这类问题的解决方案。
目标场景¶
N个测试样本分别放在如下文件夹test-001
,test-002
,...,要求:
- 为每一个测试样本生成一个测试案例
- 默认测试所有案例
- 如果输入参数
n
,则只测试前n
个案例
从pytest.fixture()开始 1¶
顾名思义,fixture
是辅助测试函数的工装夹具,其中params
参数提供了参数化输入的功能,每一个参数都会被创建为一个独立的测试案例。一个简单的例子:
# test.py
import pytest
inputs = ['test-001', 'test-002', 'test-003']
@pytest.fixture(scope="module", params=inputs)
def get_input(request):
return request.param
# 测试函数
def test(get_input):
print(f'Testing {get_input}')
这样,使用一个测试函数即可完成三个类似案例的测试。
$ pytest -sv test.py
test.py::test[test-001] Testing test-001
PASSED
test.py::test[test-002] Testing test-002
PASSED
test.py::test[test-003] Testing test-003
PASSED
再到@pytest.mark.parametrize 2¶
与fixture
类似,pytest.mark.parametrize
装饰器也提供了参数化输入的功能。fixture
例子中,测试函数的参数get_input
是一个fixture
,本例中get_input
是一个自定义的参数名称。
import pytest
inputs = ['test-001', 'test-002', 'test-003']
# 测试函数
@pytest.mark.parametrize("get_input", inputs)
def test(get_input):
print(f'Testing {get_input}')
以上两种方式都满足了 为每一个样本创建独立测试案例 的需求,然而问题是输入参数是以硬编码的方式写死的,可扩展性很差。于是,引出了下面的参数化测试方法。
动态生成参数化测试案例 3¶
pytest_generate_tests(metafunc)
是pytest自带的在收集测试函数时调用的钩子函数,以便按照自定义方式、动态地生成参数化方案。其中,metafunc
参数可以检查请求的测试上下文,
-
metafunc.fixturenames
获取所有fixture
-
metafunc.parametrize(name, params)
生成自定义测试方案
prefix = 'test-00'
def pytest_generate_tests(metafunc):
if not "get_input" in metafunc.fixturenames: return
params = [f'{prefix}{i+1}' for i in range(3)]
metafunc.parametrize("get_input", params)
# 测试函数
def test(get_input):
print(f'Testing {get_input}')
这个例子不再直接给出硬编码的test-001
~test-003
,而是根据需要动态生成的,这为实际应用带来无限可能。但是,其中测试案例个数3
还是硬编码方式写入的!为了实现终极目标,我们必须引入命令行参数。
注册命令行参数 4 5¶
pytest_addoption(parser)
是另一个pytest钩子函数,可以让用户注册一个自定义的命令行参数,然后通过fixture
或其他钩子获取用户输入。
parser.addoption
注册命令行参数-
fixture
的pytestconfig
参数获取参数:@pytest.fixture(scope='session') def user_param(pytestconfig): return pytestconfig.getoption('--param_name')
-
或者
pytest_generate_tests
的metafunc
获取参数:metafunc.config.getoption('param_name')
注意
pytest_addoption
必须写在默认的配置文件中:conftest.py。
回到本问题,创建配置文件conftext.py
,注册用户输入参数n
。
# conftest.py
def pytest_addoption(parser):
parser.addoption(
"--n",
action="store",
default=None,
help="Specify the count of test cases.")
此时,在conftext.py
所在目录下查看pytest
帮助。可以发现,pytest
成功读取了配置文件并注册了用户输入参数--n
。
$ pytest -h
...
custom options:
--n=N Specify the count of test cases.
...
接下来,与之前动态生成测试方案的例子结合:
prefix = 'test-00'
def pytest_generate_tests(metafunc):
if not "get_input" in metafunc.fixturenames: return
n = metafunc.config.getoption("n") # 获取输入参数值
n = int(n) if n is not None else 3
params = [f'{prefix}{i+1}' for i in range(n)]
metafunc.parametrize("get_input", params)
# 测试函数
def test(get_input):
print(f'Testing {get_input}')
试验一下可知满足既定需求:
-
不指定
n
时,默认运行3个测试案例;$ pytest -sv test.py test.py::test[test-001] Testing test-001 PASSED test.py::test[test-002] Testing test-002 PASSED test.py::test[test-003] Testing test-003 PASSED
-
如果指定
n
,则运行n
个测试案例。$ pytest -sv test.py --n=2 test.py::test[test-001] Testing test-001 PASSED test.py::test[test-002] Testing test-002 PASSED
从Python代码启动pytest测试¶
以上内容基于从命令行启动测试的前提,已经可以满足本文开头目标场景的需要。不过,还可以进一步,直接从代码启动测试。有什么作用呢?锦上添花,可以进一步封装成一个针对本文场景的专用测试工具。
pytest提供了从代码执行测试的函数pytest.main()
,它接受两个参数:
args
参数列表,等同于命令行模式输入的各种参数plugins
插件列表。
什么是pytest插件?
pytest插件是通过钩子函数实现的一些功能,例如前面提到的conftest.py
即为插件类型的一种:本地插件。
我们正好将conftest.py
的代码整合到一起,得到完整版:
# test.py
# 测试函数
def test(get_input):
print(f'Testing {get_input}')
# main.py
import pytest
prefix = 'test-00'
class MyPlugin:
def pytest_addoption(self, parser):
parser.addoption(
"--n",
action="store",
default=None,
help="Specify the count of test cases.")
def pytest_generate_tests(self, metafunc):
if not "get_input" in metafunc.fixturenames: return
n = metafunc.config.getoption("n") # 获取输入参数值
n = int(n) if n is not None else 3
params = [f'{prefix}{i+1}' for i in range(n)]
metafunc.parametrize("get_input", params)
if __name__=='__main__':
pytest.main(
args=['-sv', '--n=2', 'test.py'],
plugins=[MyPlugin()])
最后,python main.py
启动测试即可。
注意,虽然最后例子中直接写入--n=2
,实际上main.py
可以借助fire
等第三方库转为命令行工具,从而接受用户输入。本文略过。