Fixture概述
说到测试框架自然要说到setup
和teardown
两个方法.
setup
是用来做准备操作.一般用来初始化资源.
teardown
是用来做收尾操作.一般用于释放资源.
pytest的setup
和teardown
是利用@pytest.fixture
这个注释来完成的.不仅可以完成初始化操作,初始化后如果有数据需要给用例使用也是非常方便!
fixture方法原型如下:
1 2
| def fixture(scope="function", params=None, autouse=False, ids=None, name=None): """Decorator to mark a fixture factory function.
|
各个参数含义分别如下:
scope
:执行初始化方法的作用域
params
:设置初始化方法要传递的参数,列表类型
autouse
:默认为False
如果设置为True
则所有的用例都会激活使用它,否则需要显示激活。
ids
:参数对应的id
列表,如果不指定则会自动生成
name
:定义fixture
名称,默认是被装饰的方法名称。
SetUp
概述
通过@pytest.fixture()
注释会在执行测试用例之前初始化操作.然后直接在测试用例的方法中就可以拿到初始化返回的参数(参数名要和初始化的方法名一样)
实践案例
创建test_fixture.py
模块,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import pytest
class TestClass():
@pytest.fixture() def init_data(self): print('init data...') return 5 def test_add1(self,init_data): print('test add 1') assert init_data == 5
def test_add2(self,init_data): print('test add 2') assert init_data == 5
|
执行命令
1 2 3 4 5 6 7
| λ pytest -q -s E:\code\learning_python\pytest_test\test_fixture.py init data... test add 1 .init data... test add 2 . 2 passed in 0.02 seconds
|
根据上面的执行结果可以看出,init_data
已经被执行了两次,默认的作用域为单个测试用例方法,如果想修改作用域,则需要用到参数scope
来指定作用域。
Scope
scope
作用域主要包括以下作用域:
module
(模块):作用于一个模块内的所有class
和def
,对于所有class
和def
,setup
和teardown
只执行一次
class
(类):作用于一个class
内中的所有test
,所有用例只执行一次setup
,当所有用例执行完成后,才会执行teardown
session
(会话):
function
(方法):作用于单个测试用例,若用例没有执行(如被skip
了)或失败了,则不会执行teardown
package
(包):作用于包,该功能还是实验性的,谨慎使用。
上面的案例将作用域改为class
后,再执行测试,此时再执行初始化就执行一次了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import pytest
class TestClass():
@pytest.fixture(scope="class") def init_data(self): print('init data...') return 5
def test_add1(self,init_data): print('test add 1') assert init_data == 5
def test_add2(self,init_data): print('test add 2') assert init_data == 5
|
scope优先级
在功能方法请求中,更高范围的fixture
(比如会话)比低范围的fixture
(比如函数或类)先被实例化。相同范围的fixture
的相对顺序与测试函数中声明的顺序一致,并且在fixture
之间的依赖关系中保持一致。
实践案例
test_scope.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import pytest
@pytest.fixture(scope="session") def s1(): print('s1') pass
@pytest.fixture(scope="module") def m1(): print('m1') pass
@pytest.fixture() def f1(tmpdir): print('f1') pass
@pytest.fixture() def f2(): print('f2') pass
def test_foo(f1, m1, f2, s1): print('test_foo')
|
执行结果
1 2 3 4 5 6 7 8 9
| λ pytest -s -q E:\code\learning_python\pytest_test\test_higher_scope.py s1 m1 f1 f2 test_foo . 1 passed in 0.20 seconds
|
说明
s1
:是作用域最高的fixture
(会话)。
m1
:是第二高作用域fixture
(模块)。
tmpdir
:是一个函数作用域的fixture
, f1
需要它:因为它是f1
的依赖项,所以需要在此时实例化它。
f1
: test_foo
参数列表中的第一个函数作用域夹具。
f2
:是test_foo
参数列表中最后一个函数作用域的fixture
。
执行结果
1 2 3 4 5 6
| pytest -q -s E:\code\learning_python\pytest_test\test_fixture.py init data... test add 1 .test add 2 . 2 passed in 0.15 seconds
|
TearDown
- 在pytest中实现
teardown
有两种方式,一种是使用yield
关键字,另外一种是利用请求上下文对象的 addfinalizer
方法来注册完成函数。
- 需要注意一下,如果在设置代码,即
yield
关键字之前,期间发生异常,则不会调用teardown
代码,但是 addfinalizer
依旧会执行tearndown
内容。
实践案例
test_fixture.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import pytest
class TestClass():
@pytest.fixture(scope="class") def init_data(request): print('init data...') def teardown(): print('finished init data!') request.addfinalizer(teardown) return 5 def test_add1(self,init_data): print('test add 1') assert init_data == 5
def test_add2(self,init_data): print('test add 2') assert init_data == 5
|
在之前的案例中,我们补充使用了addfinalizer
方法来注册完成方法teardown
。
执行结果:
1 2 3 4 5 6 7
| λ pytest -s -q E:\code\learning_python\pytest_test\test_fixture_setup.py init data... test add 1 .test add 2 .finished init data!
2 passed in 0.02 seconds
|
fixture配置文件——conftest.py
如果在实现测试过程中,如果多个测试文件需要使用同一个 fixture
,那么可以将它移动到conftest.py
文件。您不需要导入您想在测试中使用的fixture
,它会被pytest
自动发现。
fixture
函数的发现从测试类开始,然后是测试模块,然后是conftest.py
文件,最后内置和第三方插件。
实践案例
创建模块 conftest.py
1 2 3 4 5 6 7 8 9
| import pytest
@pytest.fixture(scope="class") def init_data(request): print('init data...') def teardown(): print('finished init data!') request.addfinalizer(teardown) return 5
|
将前面的test_fixture
模块改写如下,去掉了之前在模块内部定义的fixture
1 2 3 4 5 6 7 8 9 10 11
| import pytest
class TestClass():
def test_add1(self,init_data): print('test add 1') assert init_data == 5
def test_add2(self,init_data): print('test add 2') assert init_data == 5
|
执行结果
1 2 3 4 5 6 7
| λ pytest -s -q E:\code\learning_python\pytest_test\test_fixture_setup.py init data... test add 1 .test add 2 .finished init data!
2 passed in 0.02 seconds
|
可以看到完成了初始化,同理,继续创建新的测试模块,在测试方法中传入对应的fixture
方法,依旧可以执行fixture
方法。
注意:fixture
配置文件名称必须是conftest.py
否则会找不到 fixture
方法
参数化的Fixture
概述
fixture
函数可以参数化,在这种情况下,它们将被多次调用,每次执行一组相关测试,即依赖于这个fixture
的测试,测试函数通常不需要知道它们的重新运行。fixture
参数化可以用于一些有多种方式配置的功能测试。
实践案例
扩展前面的例子,我们可以标记fixture
来创建两个smtp
连接实例,这将导致使用fixture
运行两次的所有测试。fixture
功能通过特殊的请求对象访问每个参数:
conftest.py
1 2 3 4 5
| @pytest.fixture(scope="class",params=['smtp.gmail.com','mail.python.org']) def smtp_connection_multi(request): import smtplib return smtplib.SMTP(request.param,587,timeout=5)
|
说明:在参数中增加一个参数param
将要测试的参数用列表存储。同时在方法中增加request
参数,参数遍历是通过request.param
来实现。
test_smtpsimple.py
1 2 3 4
| def test_ehlo_multi(smtp_connection_multi): response,msg=smtp_connection_multi.ehlo() print(response) assert response==250
|
执行结果
1 2 3 4 5 6 7 8 9 10 11
| λ pytest -s E:\code\learning_python\pytest_test\test_smtpsimple.py ============================= test session starts ============================= platform win32 -- Python 3.5.0, pytest-3.8.1, py-1.5.3, pluggy-0.7.1 rootdir: E:\, inifile: collected 2 items
..\..\..\code\learning_python\pytest_test\test_smtpsimple.py 250 .250 .
========================== 2 passed in 22.96 seconds ==========================
|
ids
- 上面案例中两个测试函数每个都运行了两次,而且是针对不同的
smtp
实例。pytest
将建立一个字符串,它是参数化fixture
中每个fixture
值的测试ID
,
- 使用
--collect-only
运行pytest
会显示生成的ID
。
1 2 3 4 5 6 7 8 9 10
| λ pytest --collect-only E:\code\learning_python\pytest_test\test_smtpsimple.py ============================= test session starts ============================= platform win32 -- Python 3.5.0, pytest-3.8.1, py-1.5.3, pluggy-0.7.1 rootdir: E:\, inifile: collected 2 items <Module 'code/learning_python/pytest_test/test_smtpsimple.py'> <Function 'test_ehlo_multi[smtp.gmail.com]'> <Function 'test_ehlo_multi[mail.python.org]'>
======================== no tests ran in 0.02 seconds =========================
|
在上面的结果中,test_ehlo[smtp.qq.com]
和test_ehlo[mail.python.org]
,这些ID
可以与-k
一起使用来选择要运行的特定实例,还可以在发生故障时识别特定实例。
1 2 3 4 5 6 7 8 9
| λ pytest --collect-only -k smtp.gmail.com E:\code\learning_python\pytest_test\test_smtpsimple.py ============================= test session starts ============================= platform win32 -- Python 3.5.0, pytest-3.8.1, py-1.5.3, pluggy-0.7.1 rootdir: E:\, inifile: collected 2 items / 1 deselected <Module 'code/learning_python/pytest_test/test_smtpsimple.py'> <Function 'test_ehlo_multi[smtp.gmail.com]'>
======================== 1 deselected in 0.02 seconds =========================
|
实践案例
数字、字符串、布尔值和None
将在测试ID
中使用其通常的字符串表示形式,对于其他对象,pytest
会根据参数名称创建一个字符串,可以通过使用ids
关键字参数来自定义用于测试ID
的字符串。
test_ids.py
1 2 3 4 5 6 7 8 9
| import pytest
@pytest.fixture(params=[0,1],ids=('spam','ham')) def a(request): return request.param
def test_a(a): pass
|
执行结果
1 2 3 4 5 6 7 8 9 10
| λ pytest --collect-only E:\code\learning_python\pytest_test\test_ids.py ============================= test session starts ============================= platform win32 -- Python 3.5.0, pytest-3.8.1, py-1.5.3, pluggy-0.7.1 rootdir: E:\, inifile: collected 2 items <Module 'code/learning_python/pytest_test/test_ids.py'> <Function 'test_a[spam]'> <Function 'test_a[ham]'>
======================== no tests ran in 0.19 seconds =========================
|
从上面的执行结果可以看出,此时的id
为我们设定的id
而不是自动生成的id
。我们可以使用参数-k
来指定执行某一个参数
1 2 3 4 5 6 7 8 9
| λ pytest -k ham --collect-only E:\code\learning_python\pytest_test\test_ids.py ============================= test session starts ============================= platform win32 -- Python 3.5.0, pytest-3.8.1, py-1.5.3, pluggy-0.7.1 rootdir: E:\, inifile: collected 2 items / 1 deselected <Module 'code/learning_python/pytest_test/test_ids.py'> <Function 'test_a[ham]'>
======================== 1 deselected in 0.03 seconds =========================
|
从上面的执行结果我们可以理解,ids
和param
其实是一种key:value
的键值对关系。-k
参数其实就是key
的缩写。我们继续扩展上面的案例,代码如下:
test_ids.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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
|
说明:
- 上面代码中,我们新建了一个方法
idfn
,该方法的作用是接收fixture
值,判断当值为0
时将返回eggs
。
- 在新建的
fixture
中引用idfn
来作为ids
参数,从而实现根据param
值来改变ids
的值。
运行结果:
1 2 3 4 5 6 7 8 9 10 11 12
| λ pytest --collect-only E:\code\learning_python\pytest_test\test_ids.py ============================= test session starts ============================= platform win32 -- Python 3.5.0, pytest-3.8.1, py-1.5.3, pluggy-0.7.1 rootdir: E:\, inifile: collected 4 items <Module 'code/learning_python/pytest_test/test_ids.py'> <Function 'test_a[spam]'> <Function 'test_a[ham]'> <Function 'test_b[eggs]'> <Function 'test_b[1]'>
======================== no tests ran in 0.18 seconds =========================
|
参考资料