一、什么是fixture
pytest的fixture,就是pytest在执行测试用例函数的时候,先去看测试函数的形参,然后去fixture中找是否有名字匹配的,如果有,则去执行fixture函数的内容,并且根据fixture函数的返回值情况,比如fixture函数有返回值,则会将此返回值传递给测试函数,作为此测试函数中当前形参的实参值传入,声明一个fixture函数只需要在函数定义上面增加@pytest.fixture()声明即可
二、fixture类似函数传值
如下,首先声明了一个fixture函数get_num,这里为了简单的演示其作用,这是打印了一行语句,然后就返回一个数字10,在实际应用中,这里可以做很复杂的处理。当pytest执行测试函数test_func的时候,首先看一下形参,这里比如get_num,然后pytest就去自己的fixture函数列表中找是否有同名的,这里发现有,于是就去执行get_num函数,此外发现get_num这个fixture函数有一个返回值10,于是就将这个返回值10拿过来赋值给当前作为参数形参的get_num的值,这也就是为什么在测试函数中可以直接使用get_num进行断言的原因
test_demo.py代码如下
import pytest
@pytest.fixture()
def get_num():
print("\nin get_num fixture...")
return 10
def test_func(get_num):
assert get_num == 9
执行结果如下:
$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 1 item
test_demo.py
in get_num fixture...
F
=============================================================================== FAILURES ===============================================================================
______________________________________________________________________________ test_func _______________________________________________________________________________
get_num = 10
def test_func(get_num):
> assert get_num == 9
E assert 10 == 9
test_demo.py:9: AssertionError
======================================================================= short test summary info ========================================================================
FAILED test_demo.py::test_func - assert 10 == 9
========================================================================== 1 failed in 0.10s ===========================================================================
三、fixture中继续调用fixture
如下,get_num2的fixture中继续调用了get_num1的fixture,然后测试函数中调用get_num2的fixture,原理都是一样大金额
test_demo.py代码如下:
import pytest
@pytest.fixture()
def get_num1():
print("\nin get_num1 fixture...")
return 100
@pytest.fixture()
def get_num2(get_num1):
print("\nin get_num2 fixture...")
return 10+get_num1
def test_func(get_num2):
assert get_num2 == 111
执行结果如下
$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 1 item
test_demo.py
in get_num1 fixture...
in get_num2 fixture...
F
=============================================================================== FAILURES ===============================================================================
______________________________________________________________________________ test_func _______________________________________________________________________________
get_num2 = 110
def test_func(get_num2):
> assert get_num2 == 111
E assert 110 == 111
test_demo.py:14: AssertionError
======================================================================= short test summary info ========================================================================
FAILED test_demo.py::test_func - assert 110 == 111
========================================================================== 1 failed in 0.10s ===========================================================================
四、测试函数同时可以调用多个fixture
如下,test_func测试函数中调用了两个fixture
test_demo.py代码如下
import pytest
@pytest.fixture()
def get_num1():
print("\nin get_num1 fixture...")
return 100
@pytest.fixture()
def get_num2():
print("\nin get_num2 fixture...")
return 200
def test_func(get_num1,get_num2):
assert int(get_num1+get_num2) == 301
执行结果如下
$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 1 item
test_demo.py
in get_num1 fixture...
in get_num2 fixture...
F
=============================================================================== FAILURES ===============================================================================
______________________________________________________________________________ test_func _______________________________________________________________________________
get_num1 = 100, get_num2 = 200
def test_func(get_num1,get_num2):
> assert int(get_num1+get_num2) == 301
E assert 300 == 301
E + where 300 = int((100 + 200))
test_demo.py:14: AssertionError
======================================================================= short test summary info ========================================================================
FAILED test_demo.py::test_func - assert 300 == 301
========================================================================== 1 failed in 0.10s ===========================================================================
五、fixture可以设置自动执行
在定义fixture函数的时候,通过将autouse设置为True即可,如下所示,虽然整个过程中都没有调用init_name这个fixture,但是通过最后执行的结果看,这个init_name的fixture明显是执行了的
test_demo.py如下
import pytest
@pytest.fixture()
def get_name():
print("\nin get_name fixture...")
return []
@pytest.fixture(autouse=True)
def init_name(get_name):
print("\nin init fixture...")
get_name.append("张无忌")
get_name.append("张三丰")
def test_func(get_name):
assert "周芷若" in get_name
执行结果如下
$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 1 item
test_demo.py
in get_name fixture...
in init fixture...
F
=============================================================================== FAILURES ===============================================================================
______________________________________________________________________________ test_func _______________________________________________________________________________
get_name = ['张无忌', '张三丰']
def test_func(get_name):
> assert "周芷若" in get_name
E AssertionError: assert '周芷若' in ['张无忌', '张三丰']
test_demo.py:15: AssertionError
======================================================================= short test summary info ========================================================================
FAILED test_demo.py::test_func - AssertionError: assert '周芷若' in ['张无忌', '张三丰']
========================================================================== 1 failed in 0.11s ===========================================================================
六、fixture的范围
在此之前介绍额fixture似乎都是在传递值,像调用函数一样,其实传递值只是fixture的一个很小的应用,fixture最重要的作用是充当类似其他测试框架中的setup和teardown,通过上述的介绍可以得出的一个结论就是,通过调用fixture,可以做到fixture函数的代码是在测试函数之前执行的,试想,如果fixture函数的代码是比如初始化环境的代码,那么此时fixture的作用就完全可以理解为自动化测试用例的setup步骤了,事实上也确实如此,pytest自动化框架确实是fixture中的代码执行认为是setup和teardown的步骤的。
因此当把fixture作为充当setup和teardown的作用,那么问题就来了,setup和teardown是有测试函数级setup和teardown,测试类级setup和teardown,测试模块级setup和teardown,以及整个测试执行最开始和最后的setup和teardown,理解到此,就可以理解fixture的范围了,fixture的范围有以下几个:
- function: 测试函数级,这个是fixture默认的范围,如果不设置即为function级别fixture
- class: 测试类级的fixture
- module: 测试模块级的fixture
- package: 测试包级别的fixture
- session: 翻译过来叫会话级别的fixture,一个会话实质上就是执行pytest命令的整个过程,即session级实质上就是执行pytest最开始和最后的setup和teardown
session,module,package级额fixture一般在conftest.py中定义,其他级别的fixture可以在conftest.py中定义也可以在具体文件中定义,如果是共享给其他文件使用则放在conftest.py中定义,如果是自己文件中独有的,则在自己的文件中定义即可
如下,conftest.py中定义session级的fixture
import pytest
@pytest.fixture(autouse=True,scope="session")
def session_fixture():
print("\nin session_fixture...")
test_demo.py中定义function级别的fixture
import pytest
@pytest.fixture(autouse=True,scope="function")
def function_fixture():
print("\nin function_fixture...")
def test_func1():
print("in test_func1...")
def test_func2():
print("in test_func2...")
def test_func3():
print("in test_func3...")
执行结果如下,可以看到session即的只会在最开始执行一次,而function级的则会在每个测试好桉树之前都会执行,这就做到了setup的功能,至于teardown的功能需要使用到yield关键字,后续继续展开
$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 3 items
test_demo.py
in session_fixture...
in function_fixture...
in test_func1...
.
in function_fixture...
in test_func2...
.
in function_fixture...
in test_func3...
.
========================================================================== 3 passed in 0.02s ===========================================================================
七、fixture通过yield关键字实现setup和teardown
fixture中,yield关键字之前的代码为setup的步骤,yield之后的代码即为teardown的步骤,yield相当于return,如果有返回值,就直接放在yield,如果没有返回值则直接使用yield即可
如conftest.py中如下:
import pytest
@pytest.fixture(autouse=True,scope="session")
def session_fixture():
print("\nin session_fixture setup ...")
yield
print("\nin session_fixture teardown ...")
test_demo.py中如下:
import pytest
@pytest.fixture(autouse=True,scope="function")
def function_fixture():
print("\nin function_fixture setup ...")
yield 10
print("\nin function_fixture teardown ...")
def test_func1():
print("in test_func1...")
def test_func2():
print("in test_func2...")
def test_func3(function_fixture):
print("in test_func3...")
assert function_fixture == 11
执行结果如下,此时已经实现了setup和teardown的功能,此外function级的fixture还返回了一个数值
$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 3 items
test_demo.py
in session_fixture setup ...
in function_fixture setup ...
in test_func1...
.
in function_fixture teardown ...
in function_fixture setup ...
in test_func2...
.
in function_fixture teardown ...
in function_fixture setup ...
in test_func3...
F
in function_fixture teardown ...
in session_fixture teardown ...
=============================================================================== FAILURES ===============================================================================
______________________________________________________________________________ test_func3 ______________________________________________________________________________
function_fixture = 10
def test_func3(function_fixture):
print("in test_func3...")
> assert function_fixture == 11
E assert 10 == 11
test_demo.py:17: AssertionError
======================================================================= short test summary info ========================================================================
FAILED test_demo.py::test_func3 - assert 10 == 11
===================================================================== 1 failed, 2 passed in 0.10s ======================================================================
八、yield的缺陷及解决方案
首先看下如下例子,test_demo.py内容如下:
import pytest
@pytest.fixture(autouse=True,scope="function")
def function_fixture():
print("\nin function_fixture setup ...")
a=1/0
yield 10
print("\nin function_fixture teardown ...")
def test_func3(function_fixture):
print("in test_func3...")
assert function_fixture == 11
执行结果如下,仔细观察就会发现,当在setup部分出错了,则teardown部分根本就不会执行了,这一点对于自动化测试来讲是有一定弊端的,比如在setup部分配置了一些基础数据,然后由于某种原因出错了,这样由于根本不会执行teardown的操作,从而导致整体环境就有残留了,这就是yield关键字存在的弊端
$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 1 item
test_demo.py
in function_fixture setup ...
E
================================================================================ ERRORS ================================================================================
_____________________________________________________________________ ERROR at setup of test_func3 _____________________________________________________________________
@pytest.fixture(autouse=True,scope="function")
def function_fixture():
print("\nin function_fixture setup ...")
> a=1/0
E ZeroDivisionError: division by zero
test_demo.py:6: ZeroDivisionError
======================================================================= short test summary info ========================================================================
ERROR test_demo.py::test_func3 - ZeroDivisionError: division by zero
=========================================================================== 1 error in 0.10s ===========================================================================
如下,通过request的addfinalizer则可以解决这个问题
test_demo.py内容如下:
import pytest
@pytest.fixture(autouse=True,scope="function")
def function_fixture(request):
def teardown_op():
print("\nin function_fixture teardown ...")
request.addfinalizer(teardown_op)
print("\nin function_fixture setup ...")
a=10/0
return 10
def test_func3(function_fixture):
print("in test_func3...")
assert function_fixture == 11
执行结果如下,可以发现虽然setup部分虽然出错了,但是teardown仍然可以正常执行
pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 1 item
test_demo.py
in function_fixture setup ...
E
in function_fixture teardown ...
================================================================================ ERRORS ================================================================================
_____________________________________________________________________ ERROR at setup of test_func3 _____________________________________________________________________
request = <SubRequest 'function_fixture' for <Function test_func3>>
@pytest.fixture(autouse=True,scope="function")
def function_fixture(request):
def teardown_op():
print("\nin function_fixture teardown ...")
request.addfinalizer(teardown_op)
print("\nin function_fixture setup ...")
> a=10/0
E ZeroDivisionError: division by zero
test_demo.py:10: ZeroDivisionError
======================================================================= short test summary info ========================================================================
ERROR test_demo.py::test_func3 - ZeroDivisionError: division by zero
=========================================================================== 1 error in 0.10s ===========================================================================
上面的优化能解决teardown不执行的问题,但是还有另外的问题,比如setup有5个步骤,teardown有对应的5个步骤,则按照上面的解决办法带来一个问题是比如setup中在第二个步骤中报错了,则此时相当于setup只成功执行了一个步骤,而上述解决方案中的teardown中不得不把5个步骤全部执行,这在一些场景中也是有问题的
为了解决上面的问题,下面的解决方案则可能稍微更好一些,即还是使用yield关键字,不过每一个fixture的setup只做原子操作,teardown也只做与setup对应的原子操作,如下代码中f1,f2,f3中的setup和teardown分别只做原子操作,假设在f2的setup中由于某种原因出错,此时f2因为setup报错了,因此f2没有必要执行teardown,而采用这种方案即使f2中setup失败了,f1的teardown仍然执行,如此则达到了只要setup成功了就会执行其对应的teardown操作
如下test_demo.py代码如下:
import pytest
@pytest.fixture(scope="function")
def f1():
print("\nin f1 fixture setup...")
yield
print("\nin f1 fixture teardown...")
@pytest.fixture(scope="function")
def f2(f1):
print("\nin f2 fixture setup...")
a=1/0
yield
print("\nin f2 fixture teardown...")
@pytest.fixture(scope="function")
def f3(f2):
print("\nin f3 fixture setup...")
yield 10
print("\nin f3 fixture teardown...")
def test_func3(f3):
print("\nin test_func3...")
assert f3 == 11
执行结果如下:
$ pytest -s
========================================================================= test session starts ==========================================================================
platform win32 -- Python 3.9.7, pytest-6.2.5, py-1.10.0, pluggy-1.0.0
rootdir: D:\src\blog\tests, configfile: pytest.ini
plugins: allure-pytest-2.9.43, caterpillar-pytest-0.0.2, hypothesis-6.31.6, forked-1.3.0, rerunfailures-10.1, xdist-2.3.0
collected 1 item
test_demo.py
in f1 fixture setup...
in f2 fixture setup...
E
in f1 fixture teardown...
================================================================================ ERRORS ================================================================================
_____________________________________________________________________ ERROR at setup of test_func3 _____________________________________________________________________
f1 = None
@pytest.fixture(scope="function")
def f2(f1):
print("\nin f2 fixture setup...")
> a=1/0
E ZeroDivisionError: division by zero
test_demo.py:12: ZeroDivisionError
======================================================================= short test summary info ========================================================================
ERROR test_demo.py::test_func3 - ZeroDivisionError: division by zero
=========================================================================== 1 error in 0.10s ===========================================================================