Pytest Fixture使用

Fixture概述

说到测试框架自然要说到setupteardown两个方法.

  • setup是用来做准备操作.一般用来初始化资源.
  • teardown是用来做收尾操作.一般用于释放资源.

pytest的setupteardown是利用@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(模块):作用于一个模块内的所有classdef,对于所有classdefsetupteardown只执行一次
  • 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 =========================

从上面的执行结果我们可以理解,idsparam其实是一种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 =========================

参考资料