Fixture概述
说到测试框架自然要说到setup和teardown两个方法.
- setup是用来做准备操作.一般用来初始化资源.
- teardown是用来做收尾操作.一般用于释放资源.
pytest的setup和teardown是利用@pytest.fixture这个注释来完成的.不仅可以完成初始化操作,初始化后如果有数据需要给用例使用也是非常方便!
fixture方法原型如下:
| 12
 
 | 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模块,代码如下:
| 12
 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
 
 | 
执行命令
| 12
 3
 4
 5
 6
 7
 
 | λ pytest  -q -s E:\code\learning_python\pytest_test\test_fixture.pyinit 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后,再执行测试,此时再执行初始化就执行一次了。
| 12
 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
| 12
 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')
 
 | 
执行结果
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | λ pytest -s -q E:\code\learning_python\pytest_test\test_higher_scope.pys1
 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。
执行结果
| 12
 3
 4
 5
 6
 
 | pytest  -q -s E:\code\learning_python\pytest_test\test_fixture.pyinit 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
| 12
 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。
执行结果:
| 12
 3
 4
 5
 6
 7
 
 | λ pytest -s -q E:\code\learning_python\pytest_test\test_fixture_setup.pyinit 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
| 12
 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
| 12
 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
 
 | 
执行结果
| 12
 3
 4
 5
 6
 7
 
 | λ pytest -s -q E:\code\learning_python\pytest_test\test_fixture_setup.pyinit 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
| 12
 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
| 12
 3
 4
 
 | def test_ehlo_multi(smtp_connection_multi):response,msg=smtp_connection_multi.ehlo()
 print(response)
 assert response==250
 
 | 
执行结果
| 12
 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。
| 12
 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一起使用来选择要运行的特定实例,还可以在发生故障时识别特定实例。
| 12
 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
| 12
 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
 
 
 | 
执行结果
| 12
 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来指定执行某一个参数
| 12
 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
| 12
 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的值。
运行结果:
| 12
 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 =========================
 
 | 
参考资料