跨类、模块、包或会话共享fixture
function
|
每一个测试用例(包括函数和方法)执行前均会执行一次
|
class
|
每一个测试类/测试函数执行前都会执行一次
|
module
|
每一个 py 文件执行前都会执行一
|
package
|
每一个 python 包执行前会执行一次
|
session
|
整个测试前执行一次
|
-
执行顺序遵循 sesstion->package->module->class->function
-
每一类 fixture 可以是多个,同类按书写先后执行
-
模块中的 fixture 对函数、方法均有效,class 中的 fixture 只对方法有效
-
每一个函数前后均会执行模块中的 class
-
在模块和类中有同名的 fixture 存在时:如果是 function,类中会覆盖模块中,其它的不会覆盖
执行顺序测试
"""conftest.py"""
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")
"""test_b.py"""
class TestClass:
def test_order(self, func, cls, mod, pack, sess, order):
assert order == ["session", "package", "module", "class", "function"]
可以自己执行试试,assert断言中可以调换顺序运行,看是否属实。
实践小说明
warning 实践使用中,建议重点使用 sesstion、module、function 级,越简单越好
小例子-fixture范围
conftest说明
回到conftest.py文件中。当然,不写conftest也是可以的,不过多个模块使用相同的fixture的时候写道conftest还是比较方便的。 Conftest是一个Python模块,它用于管理pytest插件的参数。它允许您在项目的不同部分定义和重用测试插件参数,而无需在代码中进行任何特殊调用。使用conftest.py文件可以让您的测试代码更加模块化,并且更容易维护。 :::warning 1、文件名称默认为 conftest.py 2、conftest.py 可以有多个,搜索优先级自底而上,下层会覆盖上层(特别注意) 3、conftest.py 中的 fixture 支持函数直接引用、手动调用,也可以自动适配
示例
"""目录层级"""
pythonpp
pytest_
conftest.py
test_a.py
conftest.py
test_b.py
"""示例代码"""
"""pytest_
conftest.py
@pytest.fixture
def my_name():
dic = {"name":"清安"}
print("你猜我叫什么?")
yield dic['name']
print(f"我叫{dic['name']}")
"""test_a.py"""
def test_get_name(my_name):
print(my_name)
"""pythonpp
conftest.py
import pytest
@pytest.fixture
def my_name():
dic = {"name":"拾贰"}
print("你猜我叫什么?")
yield dic['name']
print(f"我叫{dic['name']}"
"""test_b.py"""
def test_get_name(my_name):
print(my_name)
:::warning 注意目录层级下的各py文件代码,代码中皆有标注,按照指定文件复制即可。 首先我们fixture不传参,运行看看,先运行test_a.py :::
test_a.py::test_get_name 你猜我叫什么?
PASSED我叫清安
再运行test_b.py:
test_b.py::test_get_name 你猜我叫什么?
PASSED我叫拾贰
注意cd ..到pythonpp层级下,命令运行。 然后将pytest_层级下的conftest.py内容注释掉。再运行test_b.py:
test_a.py::test_get_name 你猜我叫什么?
PASSED我叫拾贰
pytest_/test_a.py::test_my_fruit_in_basket PASSED
pytest_/test_b.py::test_get_name 你猜我叫什么?
PASSED我叫拾贰
:::warning 看,pythonpp层级下的conftest.py的夹具作用到了子目录下。如果不注释pytest_下的conftest.py,在pythonpp层级下运行pytest -vs命令,那么它们将各自作用于各自的领域。 :::
test_a.py::test_get_name 你猜我叫什么?
PASSED我叫拾贰
pytest_/test_a.py::test_my_fruit_in_basket PASSED
pytest_/test_b.py::test_get_name 你猜我叫什么?
PASSED我叫清安
范围示例-class为例
:::warning 前面说了作用范围,此处以class为例,举一反三 :::
"""conftest.py"""
import pytest
@pytest.fixture(scope="class")
def my_name():
dic = {"name":"清安"}
print("你猜我叫什么?")
yield dic['name']
print(f"我叫{dic['name']}")
"""test_b.py"""
class TestA:
def test1_get_name(self,my_name):
print(f"{__class__.__name__}",my_name)
def test2_get_name(self,my_name):
print(f"{__class__.__name__}",my_name)
class TestB:
def test1_get_name(self,my_name):
print(f"{__class__.__name__}",my_name)
def test2_get_name(self, my_name):
print(f"{__class__.__name__}", my_name)
test_b.py::TestA::test1_get_name 你猜我叫什么?
TestA 清安
PASSED
test_b.py::TestA::test2_get_name TestA 清安
PASSED我叫清安
test_b.py::TestB::test1_get_name 你猜我叫什么?
TestB 清安
PASSED
test_b.py::TestB::test2_get_name TestB 清安
PASSED我叫清安
:::warning 可以看到,夹具直接作用到了类,并没有直接作用到每个测试用例,仅仅只是将值给到了每一个用例。这也是class与默认function的区别。以此类推,根据上述的文字描述,可以尝试module以及更高层的。 :::
fixture销毁方式
我们把 fixture 后置处理又叫作 fixture 的销毁,fixture 的销毁有两种方
1、yield
前面写过一部分关于yield的代码。也在迭代器与生成器中讲过。此处不做多的讲解。它与return近似而不同于return。那么它如何具有销毁的作用呢,或者说,怎么样让它可以帮助我们达到销毁的一个作用
"""conftest.py"""
@pytest.fixture(scope="function")
def user():
print("create success!")
yield "清安"
print("delete success!")
模拟一下添加一个用户以及删除用户的功能,print处可以写sql语句以及各种语句。 那么调用后呢?
def test_user(user):
print(user)
test_b.py::test_user create success!
PASSED delete success!
"""
看到了吗,先成功后,然后自动销毁,也就是删除的操作。
2、 addfinalizer
addfinalizer要比yield复杂一些。大致作用是一样的。
"""conftest.py"""
@pytest.fixture(scope="function")
def user(request):
print("create success!")
def clear():
print("delete success!")
request.addfinalizer(clear)
return '清安'
def test_user(user):
print(user)
test_b.py::test_user create success!
PASSEDdelete success!
"""
:::warning 两种方式的实现效果,我们可以认为是一样,无论哪一种方式,均遵循以下规则
-
用例如果发生异常,不影响前后置动作的执行
-
如果前置动作失败,那么后置动作不会执行 :::
fixture标志传参-marker
"""conftest.py"""
import pytest
@pytest.fixture()
def user_name(request):
maker = request.node.get_closest_marker("name")
if maker is not None:
data = maker.args[0]
else:
data = None
return data
"""test_b.py"""
@pytest.mark.name("清安")
def test_get_user(user_name):
print(user_name)
assert user_name == '清安'
test_b.py::test_get_user 清安
PASSED
"""
这样的方式会有一个warning提示,说没有注册。问题不大。 这样的方式其实就是通过mark标记后,通过request进行参数获取,并返回值,测试用例这边通过夹具机制获取到值。比较新的知识点就是获取值的方式了。
fixture参数化
"""conftest.py"""
import pytest
datas = ('清安','拾贰','詹姆斯')
@pytest.fixture(params=datas,ids=[1,2,3])
def user_name(request):
print("user_name参数:",request.param)
return request.param
"""test_b.py"""
def test_user(user_name):
print("我是:",user_name)
test_b.py::test_user[1] user_name参数: 清安
我是: 清安
PASSED
test_b.py::test_user[2] user_name参数: 拾贰
我是: 拾贰
PASSED
test_b.py::test_user[3] user_name参数: 詹姆斯
我是: 詹姆斯
PASSED
"""
在contest.py中ids参数处也可以写成列表推导式:ids=[i for i in range(1,4)]。ids就是一个标志,起一个提示的作用,告诉你用例运行到了第几个参数,也可称之为取别名。此处的传参不难理解,主要还是通过request进行,也就是代码中的request.param。 :::warning
-
fixture 可以通过设计 params,让依赖该 fixture 的用例迭代执行
-
params 数据可以为[列表],(元组),{集合},{字典}
-
params 数据要 fixture 中通过固定 request 变量来接收
:::
参数化夹具与标记的结合使用
import pytest
@pytest.fixture(params=[0,1,2,pytest.param(3,marks=pytest.mark.skip)])
def user_data(request):
return request.param
def test_user(user_data):
print(user_data)
test_a.py::test_user[0] 0
PASSED
test_a.py::test_user[1] 1
PASSED
test_a.py::test_user[2] 2
PASSED
test_a.py::test_user[3] SKIPPED (unconditional skip)
"""
看到了吗,这样的方式,可以帮助你更好的参数化与实现想要的功能。
fixture工厂模式
工厂模式有助于在单个测试中多次需要夹具结果的情况下。夹具不直接返回数据,而是返回一个生成数据的函数。然后可以在测试中多次调用此函数。简单的说就是便于重复造轮子。 例如,我想在一个测试用例里面重复注册多个用户:
"""conftest.py"""
import pytest
@pytest.fixture()
def user_func():
def use_func(name):
return {"name":name}
return use_func
"""test_b.py"""
def test_user(user_func):
user1 = user_func("清安")
user2 = user_func("拾贰")
user3 = user_func("安安")
print("注册用户:",user1['name'],user2['name'],user3['name'])
test_b.py::test_user 注册用户: 清安 拾贰 安安
"""
看着没感觉?那改一改改成有断言的:
pytest-assume插件
简单的说就是即使断言失败了,不影响后续代码的运行。接上述:
def test_user(user_func):
user1 = user_func("清安")
# assert user1
pytest.assume(user1)
user2 = user_func("拾贰")
# assert user2 == '11'
pytest.assume(user2 == '11')
user3 = user_func("安安")
# assert user2
pytest.assume(user3)
print("注册用户:",user1['name'],user2['name'],user3['name'])
________________________________assume_________________________________________
E AssertionError: assert False
..\..\venv\lib\site-packages\six.py:718: FailedAssumption
注册用户: 清安 拾贰 安安
=============================== short test summary info ========================
PASSED test_a.py::test_my_fruit_in_basket
============================== 1 failed, 1 passed in 0.18s =====================
__________________________________assert_______________________________________
E AssertionError: assert {'name': '拾贰'} == '11'
test_b.py:116: AssertionError
=============================== short test summary info =====================
PASSED test_a.py::test_my_fruit_in_basket
============================= 1 failed, 1 passed in 0.15s ====================
:::warning 看到了吗,assert失败后是不会继续运行后续的代码,而assume插件是可以跑的。上述只是一个小示例,也可以运用到每个测试用例中。 :::
模块化:使用fixture中的fixture
:::warning 除了在测试函数中使用fixture之外,fixture函数还可以使用其他fixture本身。这有助于夹具的模块化设计,并允许在许多项目中重复使用特定于框架的装置。 ::: 这跟上述中的夹具并不局限于1个有些类似,不过用法上有些不同:
"""contest.py"""
import pytest
class Create:
def __init__(self,name):
self.name = name
@pytest.fixture(scope='module')
def create(Str='清安'):
print("CREATE SUCCESS")
return Create(Str)
"""test_a.py"""
import pytest
class Delete:
def __init__(self,name):
self.name = name
@pytest.fixture(scope='module')
def delete(create):
print("DELETE SUCCESS")
return Delete(create)
def test_user(delete):
assert delete.name
print("SUCCESS")
test_a.py::test_user CREATE SUCCESS
DELETE SUCCESS
SUCCESS
PASSED
:::warning 注意上述代码,fixture中使用fixture,添加一个用户,随后删除。此处不局限于与module,也可以是是class亦或者session。 💥注意:conftest中的scope的范围不能小于test_a.py中的fixture范围,否则ScopeMismatch: You tried to access the module scoped fixture create with a session scoped request object, involved factories: :::
小结
-
fixture 的基础用法,前后置结构
-
fixture 的调用方式,包括函数引用、自动引用、手动调用
-
fixture 的 scope 范围,包括 function、class、module、package、session,了解每一种的使用及执行顺序
-
conftest.py 管理共享的 fixture
-
fixture 的安全销毁
-
fixture 的参数内容,包括标志传参,参数化(数据驱动)
-
fixture 工厂模式的使用场景
-
利用 fixture
至于fixture是否必须要写在conftest中,我的回答是不是必须的,看情况来,如果你想多模块一起使用的情况,可以写在conftest中,这样便于关于,并且同级目录下也能便于使用。此外,fixture是pytest中使用频率相当高的一个装饰器。