关于pytest框架的fixture参数化实践踩坑记录
需求
目前在写的测试框架里,有这么个需求,要在测试用例模块的同级data目录下,引用到同模块名同测试用例方法名的数据。
例如,测试模块名为test_ns.py
,测试用例的方法名为test_create_ns
,则它可以自动获取的测试数据位于./data/test_ns.toml
文件中,测试用例独占的数据为该toml文件的[[test_create_ns]]
块的数据。
文件目录结构如下:
1 | tests/csk/api/ |
其中test_ns.toml
简化内容为:
1 | [sharedata] |
初步尝试
直接写一个方法去加载这些数据很简单,例如定义名为load_data
的方法,在测试用例里调用它,即可直接使用数据:
1 | def test_create_ns(self): |
但是这样有个缺陷,即无法很好的使用Pytest的参数化功能,因为测试用例集的定义是个列表,即反序列化data为可迭代对象。
我们可以去循环迭代此data,然而Pytest会认为这只是一个测试用例;另一方面,默认情况下,若某次循环的断言没有通过,则该用例的测试过程会结束掉,这显然不是我们想要的结果。
引入参数化机制
利用Pytest框架实现最简单的参数化过程是,对测试类或者测试方法,使用@pytest.mark.parametrize()
装饰器。
稍微灵活一点的操作是,对fixture做参数化处理,形如@pytest.fixture(params=["xxx", "yyy"])
。
于是可以写这么个fixture,形式如下:
1 |
|
params
的内容是一个可迭代对象,此数据需要从我们的toml测试数据文件里取到,而解析数据这个动作的前提是,测试用例方法能够自动找到自己同名的段数据。
起初我把它也实现成了一个fixture,形式大概如下:
1 |
|
随后把这个名为_load_data
的fixture传递给之前的fixture:
1 |
|
看似逻辑正确,然而运行却报错了,输出提示表明,fixture不能作为参数传递给另一个fixture。
Collection time and testing time
Pytest的执行大致分为两个阶段,即collection time 和 testing time。
顾名思义,前一个阶段目的主要是参数化准备以及收集测试用例,后者是真正执行测试的阶段。
fixture生成是在collection time内,装饰器是在模块导入的时候执行。
collection time完成后,Pytest进入’test time’,此时,一系列的setup方法、fixture方法会被调用,前一阶段收集的测试用例方法被执行,同样,teardown方法也会在最后时刻被调用。
由此引出的关键问题是,fixture或者test case从来不会在collection time时期执行。
所以上面的错误是,我们想要在collection time内使用fixture把数据收集完成,逻辑合理,但是pytest并不支持这种实现。
关于这个问题,pytest-dev的Github issues上也有过讨论,how to get data from fixture to parametrize with @parametrize ?
如果试图直接将fixture作为方法调用,Pytest会终止运行并提示: Fixtures are not meant to be called directly。
使用pytest_generate_tests方法完成参数化处理
有了上述前提,解决问题的思路大致清晰了起来:
完整的收集数据不能放在collection time,可以在此之前,例如模块导入的时候,作为通用的方法被调用;也可以在此之后,例如test time;
数据被参数化调用解析可以在collection time,由于要能够发现模块名/方法名,所以实现这种机制,还需要依赖自省的能力。
第一条解决起来很快,将之前的fixture拆分出来,变成普通的函数,解析出全局的字典,用以存储完整数据。
第二条在查阅文档后,发现pytest_generate_tests方法提供了完备的支持。
pytest_generate_tests是Pytest框架内置的hook方法,提供了自省的能力,而且它可以发现所有的fixture名。它的使用比较直接,直接实现即可。
collection time时间段内,Pytest在每个模块中寻找名为pytest_generate_tests的方法并调用它。这个方法并非fixture,只是普通的Python函数。
它接受名为metafunc的参数,该参数也不是fixture,而是一个特殊的pytest对象,详见pytest 7.4 源码。
最终实现示例
1 |
|
在测试用例里,加上名为_case_data
的fixture:
1 | def test_create_ns(self, _case_data): |
后记
Fixtures 和 parametrization机制使得测试数据与测试方法分离开来,数据分离使得测试代码尽可能的简化,让测试的重用性与健壮性更强了。
但是使用起来还是要理解它们的运行流程和实现机制,否则很容易踩坑。