pytes夹具-fixture详解02
目录
一、使用marks标记来向夹具中传递值
使用request对象,夹具也可以使用request对象访问测试函数的标记(marks)。这招在从测试函数向夹具中传递数据的时候十分有用
import pytest
@pytest.fixture
def get_data(request):
marker=request.node.get_closest_marker('test_data')
if marker is None:
# Handle missing marker in some way...
data = None
else:
data = marker.args[0]
return data
# content of test_a.py
import pytest
@pytest.mark.test_data('aaa')
def test_01(get_data):
assert get_data=='aaa'
二、夹具工厂
“夹具工厂”模式在一个夹具的结果需要在测试中多次使用的场景中十分有用。我们不直接返回一个数据,而是返回一个生成数据的方法。这个方法可以在测试中被多次调用。可以按照需要向工厂中传递方法:
import pytest
@pytest.fixture
def make_customer_record():
def _make_customer_record(name):
return {"name": name, "orders": []}
return _make_customer_record
#content of test_d.py
def test_customer_records(make_customer_record):
customer_1 = make_customer_record("Lisa")
customer_2 = make_customer_record("Mike")
customer_3 = make_customer_record("Meredith")
print(customer_1,customer_2,customer_3)
输出:
a\test_d.py {'name': 'Lisa', 'orders': []} {'name': 'Mike', 'orders': []} {'name': 'Meredith', 'orders': []}
三、参数化夹具
# content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module",params=["smtp.qq.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print("finalizing {}".format(smtp_connection))
smtp_connection.close()
#content of test_d.py
def test_email(smtp_connection):
print(smtp_connection)
输出结果:
D:\pytest\learn01>pytest -s a/test_d.py
========================================================================================== test session starts ===========================================================================================
platform win32 -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: D:\pytest\learn01, configfile: pytest.ini
collected 2 items
a\test_d.py <smtplib.SMTP object at 0x00000000037960B8>
.finalizing <smtplib.SMTP object at 0x00000000037960B8>
<smtplib.SMTP object at 0x00000000037960B8>
.finalizing <smtplib.SMTP object at 0x00000000037960B8>
Numbers, strings, booleans 和 None 类型的参数,pytest会使用这些类型的常见字符串表达。对于其他的对象,pytest会基于参数名称生成一个字符串。我们也可以通过 ids 键值,给某一个夹具参数值自定义一个测试ID
# content of test_ids.py
import pytest
@pytest.fixture(params=[0, 1], ids=["spam", "ham"])
def a(request):
return request.param
def test_a(a):
pass
def idfn(fixture_value):
if fixture_value == 0:
return "eggs"
else:
return None
@pytest.fixture(params=[0, 1], ids=idfn)
def b(request):
return request.param
def test_b(b):
pass
输出结果:
collected 4 items
a/test_ids.py::test_a[spam] PASSED
a/test_ids.py::test_a[ham] PASSED
a/test_ids.py::test_b[eggs] PASSED
a/test_ids.py::test_b[1] PASSED
四、在参数化夹具中使用mark标记
使用 pytest.param() 标记中也可以指定标记,指定标记之后与 @pytest.mark.parametrize 标记的作用一致
import pytest
@pytest.fixture(params=[0,1,pytest.param(2,marks=pytest.mark.skip)])
def data_param(request):
return request.param
def test_param(data_param):
pass
运行结果:
a/test_ids.py::test_param[0] PASSED
a/test_ids.py::test_param[1] PASSED
a/test_ids.py::test_param[2] SKIPPED (unconditional skip)
五、模块性:在夹具中使用夹具
除了在测试函数中使用fixture之外,fixture函数还可以使用其他fixture本身。这有助于夹具的模块化设计,并允许在许多项目中重复使用特定于框架的装置
#content of conftest.py
import pytest
import smtplib
@pytest.fixture(scope="module",params=["smtp.qq.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print("finalizing {}".format(smtp_connection))
smtp_connection.close()
# content of test_sample.py
import pytest
class App:
def __init__(self, smtp_connection):
self.smtp_connection = smtp_connection
@pytest.fixture(scope="module")
def app(smtp_connection):
return App(smtp_connection)
def test_smtp_connection_exists(app):
assert app.smtp_connection
声明 app
接收先前定义的 smtp_connection
fixture并实例化 App
对象,运行结果
a/test_ids.py::test_smtp_connection_exists[smtp.qq.com] PASSED
a/test_ids.py::test_smtp_connection_exists[mail.python.org] finalizing <smtplib.SMTP object at 0x0000000003796A20>
PASSEDfinalizing <smtplib.SMTP object at 0x00000000037961D0>
由于smtp_connection是参数化的,测试会使用两个不同的APP对象来运行两次。app夹具无需关心smtp_connection夹具的参数化情况,pytest会全面的分析夹具的依赖情况。 注意,app夹具的范围是 module,它使用了同样是 module 范围的夹具 smtp_connection夹具。如果 smtp_connection 是session范围,例子也可以正常运行,一个夹具要使用另一个夹具,应该使用范围比自己更广的夹具,但是反过来就不行,使用一个范围比自己小的夹具是没有意义的。
六、根据夹具对象分组测试
pytest在测试执行的时候会保留最少的夹具对象。如果你有一个参数化夹具,则所有使用这个夹具的测试会使用同一个夹具对象,然后在下一个夹具产生之前调用终结器销毁。另一方面,这样能够更方便的创建和使用全局状态: 下面的例子使用了两个参数化的夹具,其中一个是module级别的,所有的函数都打印了调用,以显示 setup/teardown 的流
import pytest
@pytest.fixture(scope="module", params=["mod1", "mod2"])
def modarg(request):
param = request.param
print(" SETUP module", param)
yield param
print(" TEARDOWN module", param)
@pytest.fixture(scope="function", params=[1, 2])
def otherarg(request):
param = request.param
print(" SETUP function", param)
yield param
print(" TEARDOWN function", param)
def test_2(otherarg, modarg):
print(" RUN test2 with function {} and module {}".format(otherarg, modarg))
a/test_ids.py::test_2[mod1-1] SETUP module mod1
SETUP function 1
RUN test2 with function 1 and module mod1
PASSED TEARDOWN function 1
a/test_ids.py::test_2[mod1-2] SETUP function 2
RUN test2 with function 2 and module mod1
PASSED TEARDOWN function 2
a/test_ids.py::test_2[mod2-1] TEARDOWN module mod1
SETUP module mod2
SETUP function 1
RUN test2 with function 1 and module mod2
PASSED TEARDOWN function 1
a/test_ids.py::test_2[mod2-2] SETUP function 2
RUN test2 with function 2 and module mod2
PASSED TEARDOWN function 2
TEARDOWN module mod2
由此可以看出,module 范围的参数化夹具 modarg之后,在执行function范围的参数化夹具,在mod2 执行setup之前,mod1的结束器被调用了。
七、使用usefixtures在类和模块中使用夹具
#content of conftest.py
import pytest
@pytest.fixture
def get_attr():
l=range(3)
print("初始化列表:",l)
yield l
l=[]
import pytest
@pytest.mark.usefixtures("get_attr")
class TestList():
def test_a(self):
pass
assert 1
def test_b(self):
pass
assert 1
输出结果:
a/test_ids.py::TestList::test_a 初始化列表: range(0, 3)
PASSED
a/test_ids.py::TestList::test_b 初始化列表: range(0, 3)
PASSED
通过使用 usefixtures 标记, 夹具会被每个测试方法执行,就跟你给每个测试方法都使用get_attr作为参数的效果是一样的 注意夹具的默认执行范围是function,所以这里虽然userfixture是标在class上面的,但是仍然每个方法都会执行
标记使用多个夹具
@pytest.mark.usefixtures("get1", "get2") def test(): ...
还可以指定在测试模块的级别使用pytestmark指定夹具:
pytestmark = pytest.mark.usefixtures("cleandir")
译者注:之前的例子是usefixture来修饰一个类,这个例子是在一个模块中,即使没有类,把上面一行代码加入到一个模块的代码之中,pytestmark无需调用,夹具就会自动的被所有测试函数使用,但是注意: pytestmark 这个变量值不能随意指定,必须是 pytestmark通过在ini配置文件中指定使用一个全局的夹具:
# content of pytest.ini
[pytest]
usefixtures = cleandir
八、自动使用的夹具
有时候可能希望一个夹具自动的被插入到我们的测试中,不需要在函数参数中指定,也不需要 usefixtures 来修饰。例如下方在每个测试文件中都初始化一个列表
#content of conftest.py
import pytest
@pytest.fixture(autouse=True)
def get_attr():
l=range(3)
print("初始化列表:",l)
yield l
l=[]
def test_a():
pass
assert 1
def test_b():
pass
assert 1
输出结果:
a/test_ids.py::test_a 初始化列表: range(0, 3)
PASSED
a/test_ids.py::test_b 初始化列表: range(0, 3)
PASSED
这个function级别的get_attr夹具被标记为 autouse=true,意味着测试文件中的每一个方法都将自动使用get_attr夹具
下面是在其他作用域下autouse夹具如何生效的说明:
- autouse 的夹具遵从 scope 的定义规则:如果一个自动使用的夹具被定义为 session级别,不管它定义在哪里,都只会被使用执行一次。范围为 类class 表明它会在每个类中运行一次。
- 如果在模块中定义了一个自动使用的夹具,这个模块的所有测试方法都会使用它
- 如果一个自动使用的夹具定义在 conftest.py 文件中,所有在这个目录和子目录的测试都会使用这个夹具
- 最后,使用autouse的时候需要小心一点:如果在插件中定义了一个自动使用的夹具,只要是安装了这个插件的所有模块中的所有测试都会使用它。如果说夹具只能在某种设置下生效或需要某个配置,比如ini文件中的配置的时候,这种方式是有用的。这样的一个全局的夹具应该是轻量级的,应该避免其做过多的工作,避免其额外的impoet和过慢的计算。 注意上面 transact 夹具很可能是你再项目中会用到的夹具,但是不一定希望他一直处于激活的状态。对于这种情况标准的做法是把 transact 的定义放到conftest.py 中,不要标记自动使用:
# content of conftest.py
@pytest.fixture
def transact(request, db):
db.begin()
yield
db.rollback()
在类 TestClass 需要使用的时候,指定的使用夹具
@pytest.mark.usefixtures("transact") class TestClass: def test_method1(self): ...
TestClass中的所有测试都会使用 transaction夹具,其他类或模块中的函数则不会使用这个夹具,除非他们也显式的指明要使用夹具。
九、覆盖不同级别的夹具
1、在文件夹(conftest)层面复写夹具
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
def test_username(username):
assert username == 'username'
subfolder/
__init__.py
conftest.py
# content of tests/subfolder/conftest.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
test_something.py
# content of tests/subfolder/test_something.py
def test_username(username):
assert username == 'overridden-username'
同一个名字的夹具会被下级的夹具覆盖,注意,上级的夹具能够非常轻易的被下级夹具调用到,就像上面的例子中展示的那样。
2、 在module的级别复写夹具
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture
def username():
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-' + username
def test_username(username):
assert username == 'overridden-username'
test_something_else.py
# content of tests/test_something_else.py
import pytest
@pytest.fixture
def username(username):
return 'overridden-else-' + username
def test_username(username):
assert username == 'overridden-else-username'
module的层面可以使用相同的夹具名字覆盖更高层级的夹具的行为
3、使用直接的参数化方式复写夹具
import pytest
@pytest.fixture
def username():
return 'username'
@pytest.fixture
def other_username(username):
return 'other-' + username
import pytest
@pytest.mark.parametrize('username', ['directly-overridden-username'])
def test_username(username):
assert username == 'directly-overridden-username'
@pytest.mark.parametrize('username', ['directly-overridden-username-other'])
def test_username_other(other_username):
assert other_username == 'other-directly-overridden-username-other'
输出:
a/test_ids.py::test_username[directly-overridden-username] PASSED
a/test_ids.py::test_username_other[directly-overridden-username-other] PASSED
使用参数值复写了夹具的返回值。注意,即使是没有直接使用的夹具也可以被复写,就像上面最test_username_other测试一样。
4、参数化夹具和非参数化夹具的相互覆盖
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@pytest.fixture(params=['one', 'two', 'three'])
def parametrized_username(request):
return request.param
@pytest.fixture
def non_parametrized_username(request):
return 'username'
test_something.py
# content of tests/test_something.py
import pytest
@pytest.fixture
def parametrized_username():
return 'overridden-username'
@pytest.fixture(params=['one', 'two', 'three'])
def non_parametrized_username(request):
return request.param
def test_username(parametrized_username):
assert parametrized_username == 'overridden-username'
def test_parametrized_username(non_parametrized_username):
assert non_parametrized_username in ['one', 'two', 'three']
test_something_else.py
# content of tests/test_something_else.py
def test_username(parametrized_username):
assert parametrized_username in ['one', 'two', 'three']
def test_username(non_parametrized_username):
assert non_parametrized_username == 'username'
个参数化的夹具可以被不是参数化的夹具复写,反之亦然
参考文档:https://blog.csdn.net/nxy_wuhao/article/details/115722366?spm=1001.2014.3001.5501