1. fixture的作用域
1.1 scope
通过前面文章的介绍,我们知道@pytest.fixture()有一个scope参数,并且有五个作用域级别,这五个作用域级别的说明总结如下:
注意:fixture是在测试首次请求时创建的,并基于它们的作用域被销毁。
scope | 说明 |
---|
function | 默认值,函数或方法级别被调用,当函数执行结束后,fixture则被销毁 | class | 类级别调用一次,当类中的最后一个测试函数执行结束后,fixture则被销毁 | module | 模块级别(每一个.py文件)调用一次,当模块中最后一个测试函数执行结束后,fixture则被销毁 | package | 包(一个文件夹下的.py文件)级别调用一次,当包中最后一个测试函数执行结束后,fixture则被销毁 | session | 多个文件调用一次,当多文件中最后一个测试函数执行结束后,fixture则被销毁 |
单看说明,还是不太好理解的,所以下面我们通过示例来了解一下scope的用法。
示例1:
1.function:
演示代码:
import pytest
@pytest.fixture()
def login():
print('登陆操作')
yield
print('注销操作')
class TestClass:
def test_case1(self,login):
print("TestClass:test_case1")
def test_case2(self):
print("TestClass:test_case2")
运行结果: 说明:
演示代码中,我们定义了一个fixture函数——login 和两个测试函数 test_case1 和test_case2 。
装饰器@pytest.fixture() 没有指定scope 参数,因此使用默认值:function 。
test_case1 和test_case2 在TestClass 类中,且test_case1 调用了login ,test_case2 未调用。
通过运行结果,我们可以看出来,scope=function 的fixture——login ,只在调用了它的test_case1 这个测试函数前后执行。test_case2 未执行。
2.class:
演示代码:
import pytest
@pytest.fixture(scope='class')
def login():
print('登陆操作')
yield
print('注销操作')
class TestClass1:
def test_case1(self,login):
print("TestClass1:test_case1")
def test_case2(self):
print("TestClass1:test_case2")
class TestClass2:
def test_case1(self):
print("TestClass2:test_case1")
def test_case2(self,login):
print("TestClass2:test_case2")
@pytest.mark.usefixtures("login")
class TestClass3:
def test_case1(self):
print("TestClass3:test_case1")
def test_case2(self):
print("TestClass3:test_case2")
运行结果: 说明:
演示代码中我们定义了一个fixture——login ;三个测试类——TestClass1 、TestClass2 和TestClass3 ,这个三个测试类中都定了两个测试函数——test_case1 和test_case2 。
对fixture函数我们声明了其作用域——scope='class' 。 TestClass1 中,只有test_case1 调用了login ; TestClass2 中,只有test_case1 调用了login ; TestClass3 ,我们使用@pytest.mark.usefixtures("login") ,让整个类调用了login 。
查看运行结果我们可以发现: TestClass1 中,test_case1 运行前有”登陆操作“打印,并在test_case2 执行后有”注销操作“显打印。 TestClass2 中,只有test_case2 运行前有”登陆操作“打印,并在test_case2 执行后有”注销操作“打印。而test_case1 执行前后并无任何数据打印。 TestClass3 则与Testclass1 执行后数据显示一致。
TestClass1 和TestClass2 结果的不同是由什么原因造成的呢? 官方文档在介绍fixture的scope的时候,有这样一句话,上文中也有提到:fixture是在测试首次请求时创建的,并基于它们的作用域被销毁。(Fixtures are created when first requested by a test, and are destroyed based on their scope。) 对于TestClass1 ,test_case1 调用了login ,因此会在test_case1 执行的时候,创建login ,并在其执行之前打印了”登陆操作“。又根据login 的scope——class ,在TestClass1 的最后一个测试函数test_case2 执行后,根据login 的功能打印”注销操作“,并销毁login 。 对于TestClass2 ,login 是由test_case2 调用的,而test_case1 比test_case2 先执行,所以test_case1 执行的时候login 还未被调用,fixture未被创建。直到执行test_case2 时,login 才被创建并进行了对应的操作。 后面其他类型的作用域示例中将不会在演示此种情况。
3.module:
演示代码:
import pytest
@pytest.fixture(scope='module')
def login():
print('登陆操作')
yield
print('注销操作')
@pytest.mark.usefixtures("login")
class TestClass1:
def test_case1(self):
print("TestClass1:test_case1")
def test_case2(self):
print("TestClass1:test_case2")
class TestClass2:
def test_case1(self):
print("TestClass2:test_case1")
def test_case2(self):
print("TestClass2:test_case2")
运行结果: 说明:
演示代码中,我们定义了: 一个fixture——login() ,并且声明其scope=”module“ ; 两个测试类——TestClass1 和TestClass2 ; 两个测试类中分别定义了两个测试函数——test_case1 和test_case2 ; TestClass1 使用@pytest.mark.usefixtures("login") 的方法,调用了login() 。
运行结果显示: 执行时,自第一个测试函数TestClass1 中的test_case1 调用login() 时,该fixture被创建,并且按照login() 实现的功能打印"登陆操作",并在当前模块中(一个.py文件)最后一个测试函数TestClass2 中的test_case2 执行结束后,按照login() 实现的功能打印"注销操作",然后被销毁。
4.package:
演示代码:
首先看一下测试包目录,新增一个conftest.py 。里面所有的模块都是上面的演示代码。修改这些模块里代码,注释调feature函数,删掉调用即可。
conftest.py
import pytest
@pytest.fixture(scope='package',autouse=True)
def login():
print('登陆操作')
yield
print('注销操作')
使用命令行运行: 运行结果:
说明: conftest.py中声明了一个fixture——login() ,并且设置scope='package' ,autouse=True 。
使用命令行方式运行test_scope 这个测试包中的所有模块。查看运行结果,在test_scope 中第一个测试函数调用login() 的时候,login创建,并按照功能打印”登陆操作“。在test_scope 中最后一个测试函数运行结束后,按照login() 功能打印出”注销操作“,并销毁login() 。
5.session:
使用package的演示代码,需改conftest.py 中login() 的scope='session' ,然后我们直接在终端中使用命令行运行多个文件。
运行以下命令:
运行结果: 说明:
使用命令行方式运行test_scope 这个测试包中的test_class.py和test_function.py。查看运行结果,在test_class.py 中第一个测试函数调用login() 的时候,login创建,并按照功能打印”登陆操作“。在test_function.py 中最后一个测试函数运行结束后,按照login() 功能打印出”注销操作“,并销毁login()
1.2 动态作用域(Dynamic scope)
在某些情况下,我希望在不更改代码的情况下更改feature的作用域。pytest的从5.2版本开始,提供了动态作用域的方法,来解决这种情况。
通过官方文档的说明,我们了解到,可以将一个可调用对象传递给scope来实现动态作用域。这个可调用对象必须满足以下三个条件:
1.这个可调用对象返回类型需是string ,且必须是有效的scope级别,即只能返回"function" 、"class" 、"module" 、"package" 、"session" 中的一个。 2.在fixture定义期间,这个可调用对象只能被执行一次,不能被多次调用。 3.这个可调用对象定义的时候,必须使用fixture_name 和config 作为参数。
下面通过示例给大家演示以下用法。
示例2:
演示代码:
import pytest
def dynamic_fixture_scope(fixture_name,config):
if config.getoption("-k",None):
return "function"
return "class"
@pytest.fixture(scope=dynamic_fixture_scope,autouse=True)
def login():
print('登陆操作')
yield
print('注销操作')
class TestClass1:
def test_A(self):
print("这是TestClass1:test_A")
def test_B(self):
print("这是TestClass1:test_B")
class TestClass2:
def test_A(self):
print("这是TestClass2:test_A")
说明:
dynamic_fixture_scope :用来管理fixture的作用域,这个函数按照pytest官方文档的要求,带有fixture_name 和config 两个关键字作为参数,并且实现下述功能:
- 当用命令行运行测试时,运行命令中带有
‘-k’ 参数,则被调的fixture函数的作用域为"function" , - 其余情况下,被调的fixture的函数为
"class" 。
login :一个使用动态作用域方法的fixture,其scope=dynamic_fixture_scope 。为了方便演示,autouse 也被设置为True 。
下面我们分两种情况来运行看看:
1.带参数-k 运行
运行命令:我们只运行测试名称中包含test_A 的测试函数。
pytest -vs -k 'test_A' test_dynamicscope.py
运行结果: 通过运行结果可以看到,当命令行中出现-k 参数的时候,login 的scope 确实是function 级别的。两个被选中的测试函数分别在不同的测试类中,但是都调用了login 。
2.无参数-k 运行
运行命令:我们不带参数-k 运行。
pytest -vs test_dynamicscope.py
运行结果: 通过运行结果可以看到,无-k 参数的时候,login 的scope 确实是class 级别的。
2. fixture的实例化顺序
根据官方文档,我们了解到影响fixture实例化顺序的三个因素是:
1. 作用域(scope) 2. 依赖项 3. 自动调用(autouse)
而fixture名称、测试函数名称以及定义它们的位置一般都不会影响执行顺序。
下面介绍一下这个三个因素是如何影响实例化顺序的。
2.1 作用域级别高的fixture先执行
我们直接使用官方文档提供的示例来说明。
示例3:
演示代码:
import pytest
@pytest.fixture(scope="session")
def order():
return []
@pytest.fixture
def func(order):
order.append("function")
@pytest.fixture(scope="class")
def cls(order):
order.append("class")
@pytest.fixture(scope="module")
def mod(order):
order.append("module")
@pytest.fixture(scope="package")
def pack(order):
order.append("package")
@pytest.fixture(scope="session")
def sess(order):
order.append("session")
class TestClass:
def test_order(self, func, cls, mod, pack, sess, order):
assert order == ["session", "package", "module", "class", "function"]
运行结果: 说明: 演示代码中我们定义了: 六个fixture函数:
order :scope为session级别,返回一个空list。func : 调用了order,scope为默认的function级别,并实现向order返回的列表中插入”function“的操作。cls :调用了order,scope为class级别,并实现向order返回的列表中插入”class“的操作。mod :调用了order,scope为module级别,并实现向order返回的列表中插入”module“的操作。pack :调用了order,scope为package级别,并实现向order返回的列表中插入”package“的操作。sess :调用了order,scope为session级别,并实现向order返回的列表中插入”session“的操作。
一个测试函数: test_order :主要目的是通过list中元素的顺序来判断是否按照预想的顺序执行fixture。
根据运行结果显示,测试断言通过,也就是fixture的执行顺序是按照我们预期的scope的级别由高到低去执行的。
test_order 调用fixture的顺序是func , cls , mod , pack , sess , order ,而实际执行的顺序是order , sess , pack , mod , cls , func 。由此可见,我们在调用时定义的顺序不会影响到fixture的实际执行顺序的。
官网执行顺序图:
其中sess 与order 的scope都是session级别的,但是因为order 是sess的依赖项,所以会先调用order ,这个一点正好我们下面要说明。
2.2 fixture函数的依赖项先执行
前面文章中有说过,fixture函数是可以调用另外一个fixture函数的。在执行的时候,也是先执行调用的那个fixture函数。举了例子,fixture a 调用了fixture b,当测试函数调用fixture a的时候,会先去执行fixture b 然后再执行fixture a,最后执行测试函数。
这是因为fixture b 是fixture a 的依赖项,fixture a 可能会需要fixture b提供一些条件才能正常被执行。
下面我们来看一下官方文档提供的一个例子,
示例4:
演示代码:
import pytest
@pytest.fixture
def order():
return []
@pytest.fixture
def a(order):
order.append("a")
@pytest.fixture
def b(a, order):
order.append("b")
@pytest.fixture
def c(a, b, order):
order.append("c")
@pytest.fixture
def d(c, b, order):
order.append("d")
@pytest.fixture
def e(d, b, order):
order.append("e")
@pytest.fixture
def f(e, order):
order.append("f")
@pytest.fixture
def g(f, c, order):
order.append("g")
def test_order(g, order):
assert order == ["a", "b", "c", "d", "e", "f", "g"]
运行结果: 说明:
演示代码中定义了八个fixture函数和一个测试函数,其中: fixture函数有:orde 、a 、b 、c 、d 、e 、f 、g 测试函数有:test_order
a 调用了order ; b 调用了a , order ; c 调用了a , b , order ; d 调用了c , b , order ; f 调用了e , order ; g 调用了f , c , order ; test_order 调用了g 、order 。
我们来整理以上所有函数的依赖关系,他们之间的依赖关系如图所示: 之前文章也有说过,在同一次测试执行过程中,fixture是可以被多次调用的,但是不会被多次执行。执行一次后,fixture的实例对象和返回值会存在缓存中,下次再被调用的时候是直接从缓存中获取的。
所以上面的顺序我们可以再简化一下: 执行完所有依赖的fixture函数后,我们得到的order的结果为:['a','b','c','d','e'] ,因此测试断言通过。
2.3 自动使用的fixture在其作用域内首先执行
根据官方文档的说明,我们可以得到一下两点重要内容:
1.当测试函数调用的fixture的作用域级别都一样,那么会首先调用自动使用的fixture。
示例5:
import pytest
@pytest.fixture
def order():
return []
@pytest.fixture
def func(order):
order.append("function")
@pytest.fixture(autouse=True)
def cls(order):
order.append("class")
@pytest.fixture
def mod(order):
order.append("module")
@pytest.fixture
def pack(order):
order.append("package")
@pytest.fixture
def sess(order):
order.append("session")
class TestClass:
def test_order(self, func, cls, mod, pack, sess, order):
print(order)
assert order == ["session", "package", "module", "class", "function"]
运行结果: 说明:
我们把示例3的代码拿过来修改一下,把所有fixture的scope都改为function级别,并且对cls试着autouse=True。 test_order请求fixture的顺序是:func, cls, mod, pack, sess 。
当scope级别一样的时候,且无依赖关系的时候,fixture的执行顺序应该与调用顺序一致,也应该是func, cls, mod, pack, sess 。但是实际运行的结果却是['class', 'function', 'module', 'package', 'session'] 。由此可以判断出,在scope一致且无依赖的情况下,自动执行的fixture是最先被执行的。
2.对一个autouse的fixture A来说,若调用了非autouse的fixture B,那么对于调用了fixture A的测试函数来说,fixture B也相当于是autouse的。
我们再来看一下官方给的一个示例。
示例6:
演示代码:
import pytest
@pytest.fixture
def order():
return []
@pytest.fixture
def a(order):
order.append("a")
@pytest.fixture
def b(a, order):
order.append("b")
@pytest.fixture(autouse=True)
def c(b, order):
order.append("c")
@pytest.fixture
def d(b, order):
order.append("d")
@pytest.fixture
def e(d, order):
order.append("e")
@pytest.fixture
def f(e, order):
order.append("f")
@pytest.fixture
def g(f, c, order):
order.append("g")
def test_order_and_g(g, order):
assert order == ["a", "b", "c", "d", "e", "f", "g"]
说明:
演示代码中定义了八个fixture函数和一个测试函数,其中: fixture函数有:orde 、a 、b 、c 、d 、e 、f 、g 测试函数有:test_order
a 调用了order ; b 调用了a , order ; c 调用了 b , order ,且是自动使用fixture; d 调用了 b , order ; f 调用了e , order ; g 调用了f , c , order ; test_order 调用了g 、order 。
若c是非自动使用的fixture,代码中fixture的一个执行顺序是什么样的呢?
下图是运行结果:
根据上面的运行结果和下面的顺序图,我们可以看到,除了g调用了c之外,其他函数都没有调用c,而g也调用了f,所以现在还不清楚c应该在d、e、f之前还是之后执行。对c来说他只需在b之后、g之前执行即可。而实际运行结果,c是在d、e、f之后和g之前执行的。
对于pytest来说,fixture的执行顺序就是不明确,这种情况很可能会影响测试函数的执行行为,影响了测试结果。 所以,我们需要明确fixture的执行顺序。
当我们使c为自动使用fixture时,这个顺序又会发生什么变化呢?
下图为运行结果:
根据上图的运行结果和下图的顺序图,我们可以看出来,当自动使用c后,它的执行优先级就比d要高,所以可以保证c是在d之前执行的。这样使执行顺序明确了。
当c是自动使用的fixture后,根据fixture函数的依赖项先执行这一规则,c所依赖的b、b所依赖的a和a所依赖的order都会被执行。这样对于调用了c的g来说,order、a、b也相当于是自动使用的fixture了,但是当有其他fixture调用order、a、b时,order、a、b仍然保持自己的特性。
本节总结
我们来总结一下fixture实例化顺序的一个规则:
- fixture的scope级别越高,那么它执行的优先级越高。优先级为:session>package>module>class>function
- fixture如果存在依赖项,那么它所依赖的fixture函数会先被执行。
- 同scope级别fixture中,自动使用的fixture会最先执行;若该fixture存在依赖项,则对于调用了fixture的测试函数来说,这些依赖项也可以看做是自动使用的。
- 我们在编码的时候最好可以依照以上规则为pytest提供一个清晰的fixture执行顺序,以免影响测试函数的行为和测试结果。
3. fixture的可用性
对于测试函数来说,能够调用那些fixture,是由fixture定义的位置决定。
1.当fixture定义在一个测试类中时,只有该类中的测试函数可调用此fixture,其他测试函数无法调用该fixture。当测试函数在模块的全局范围内定,则模块下的所有测试函数都可以调用它。该特点与全局变量和局部变量相似。
示例7:
演示代码:
import pytest
class TestClass1:
@pytest.fixture()
def login(self):
print("login")
def test_case1(self,login):
print("TestClass1::test_case1")
class TestClass2:
def test_case2(self,login):
print("test_case2")
运行结果: 说明:
演示代码中定义了两个测试类:TestClass1 和TestClass2 。 TestClass1 中定义了一个fixture——login ,和测试函数test_case1 ,test_case1 有调用login 。 TestClass1 中定义了一个测试函数test_case2 ,test_case2 有调用login 。
通过运行结果我们可以看到,与login 在同一测试类中的test_case1 成功调用了login ,但是TestClass2 中test_case2 调用login 的时候报错了,报错原因:未发现fixture函数“login” 。
2.conftest.py中定义的fixture,可以被同目录下的所有模块中定义的测试函数调用。
示例8:
演示代码:
test_availability/conftest.py:
import pytest
@pytest.fixture()
def login():
print("login")
test_availability/test_availability.py:
class TestClass1:
def test_case1(self,login):
print("TestClass1::test_case1")
class TestClass2:
def test_case2(self,login):
print("test_case2")
运行结果: 说明:
我们在test_availability目录下创建了一个conftest.py,并且定义了一个fixture——login 。 在test_availability.py中,我们定义了两个测试函数test_case1 和test_case1 ,两个测试函数都调用了login 。
查看运行结果,测试函数都成功调用了login 。
3.如果安装的第三方插件提供了fixture,任何测试函数都可以正常使用,无需考虑位置因素。
文末说明: 以上内容是我在阅读pytest官方文档后,依照个人理解进行整理。内容可能会有理解错误之处,欢迎大家留言指正。谢谢!(*?▽?*)
|