小门板儿

Menu

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夹具如何生效的说明:

# 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

— 于 共写了10163个字
— 标签:

评论已关闭。