背景
本文总结pytest中极具特色和功能强大的fixture。
说明
本文将从以下几点进行总结:
- fixture的概念和作用
- fixture的参数
- 如何使用fixture
- fixture的优先级
- fixture的作用范围
- fixture的autouse参数说明
- fixture的重命名
- fixture在测试用例结束后清除、恢复等行为
- fixture调用fixture
- 利用fixture传递参数
fixture的概念和作用
参考unittest框架中的setup\teardown的概念来理解fixture。
setup\teardown的作用范围是全局的。比如其概念是在测试用例的开始和结束被执行,那么该测试类中的所有测试用例都在开始前都会执行setup,在结束前都会执行setdown。
如果有这样一种场景,某个前置条件,只需要在某个用例开始前完成,在另一个用例开始前不需要甚至不允许被完成,setup的局限性就出来了。
fixture的作用范围可以指定到具体的某一个测试用例,而不是测试文件中全部测试用例。
其作用范围的具体内容将在下面总结。
以上,是pytest中fixture概念比unittest优秀的地方,相信这是大多数人在二者中选择pytest的原因。
fixture的概念是:
在测试函数的运行前后,由pytest运行的外壳函数。fixture是为替代测试用例完成导入、设置、清理工作而设计,使得测试用例可以专注测试步骤本身。fixture的代码可以定制,可以模块化。每个内置fixture有明确的名称,自定义fixture可以自定义fixture名称,pytest通过fixture名称来使用fixture。
fixture的作用是:
配置测试前系统的初始状态、完成测试用例的前置条件,传递测试数据,测试用例完成后对测试环境的请理、复原。
fixture的参数
fixture有以下参数:
fixture(scope="function", params=None, autouse=False, ids=None, name=None):
scope:
控制fixture生效的频率。准确设置scope有助于节省多次创建fixture产生的时间。
可取四个值:
function(函数级别):scope的默认值,每个测试函数可以且只需要运行一次该fixture。
class(测试类级别):每个测试类只需要运行一次该fixture,测试类中的方法都可以共享这个fixture。意味着fixture只需要在测试类级别创建一次,那么就可以供给多个测试方法复用。
module(测试模块级别):同理,每个测试模块只需要运行一次该fixture,该模块中的测试类、测试类中方法、测试函数都可以复用该fixture,而不需要反复创建。
session(会话级别):同理,会话级别的fixture每个测试会话只需要运行一次,那么该次pytest测试会话中的所有方法、函数都已复用该fixture。
params:
fixture函数的参数列表,fixture的参数化会用到这个参数。如果该参数列表中有N个参数,那么意味着调用该fixture时,会使得该fixture执行N次(以传入每个参数执行一次),那么同理可得,调用该fixture的测试用例也会执行N次。
autouse:
如果该参数设置为True,则scope指定的作用范围内,该fixture将默认被执行,而不需要通过名称来使用。
ids:
一个id列表,列表长度与params列表对应。其作用是为传入不同的参数运行的fixture指定相应的id。如果没有该id列表,那么当fixture参数化运行时,pytest使用fixture名加一串数字作为fixture标识。
name:
用来指定fixture的名字,当该参数没有被指定时,fixture的函数名就是fixture的名字。
如何使用fixture
使用fixture的两种方式:
- 在测试函数或测试方法的参数列表中指定fixture名字,即把fixture名字作为测试用例的传入参数。适用于测试函数和测试类中的测试方法。该方式可以使得测试函数或测试方法使用fixture的返回值。
- 使用usefixtures指定fixture。如
@pytest.mark.usefixtures('fixture1','fixture2')
。使用该标记来标记测试函数(方法)或测试类。该方法与上个方法相比,区别在于该方法无法使用fixture的返回值。但该方法非常适合对测试类使用。
示例1:
在测试函数或测试方法的参数列表中指定fixture名字
# ./conftest.py
import pytest
@pytest.fixture()
def fixture_1():
print("我是fixture_1")
return("我是fixture_1的返回值")
# ./test_case/test_func.py
import pytest
from func import *
class TestFunc:
# 正常测试用例
def test_add_by_class(self,fixture_1):
assert add(2,3) == 5
def test_add_by_func_aaa(fixture_1):
assert 'aaa' == 'aaa'
relust = fixture_1
print(relust)
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 2 items
test_case/test_func.py::TestFunc::test_add_by_class 我是fixture_1
PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_1
我是fixture_1的返回值
PASSED
============================== 2 passed in 0.04s ==============================
[Finished in 1.4s]
'''
示例2:
使用usefixtures指定fixture。
# ./conftest.py
import pytest
@pytest.fixture(scope='class')
def fixture_1():
print("我是fixture_1")
return("我是fixture_1的返回值")
# ./test_case/test_func.py
import pytest
from func import *
@pytest.mark.usefixtures('fixture_1')
class TestFunc:
# 正常测试用例
def test_add_by_class(self,fixture_1):
assert add(2,3) == 5
print(fixture_1)
def test_add_by_class_11(self,fixture_1):
assert add(2,3) == 5
print(fixture_1)
def test_add_by_func_aaa(fixture_1):
assert 'aaa' == 'aaa'
print(fixture_1)
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
stdout:
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 3 items
test_case/test_func.py::TestFunc::test_add_by_class 我是fixture_1
我是fixture_1的返回值
PASSED
test_case/test_func.py::TestFunc::test_add_by_class_11 我是fixture_1的返回值
PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_1
我是fixture_1的返回值
PASSED
============================== 3 passed in 0.04s ==============================
[Finished in 1.4s]
'''
Tips:
本示例2中fixture_1是测试类级别的,虽然我们在测试类和测试类中的方法都使用的fixture_1,但从stdout中可以看出,测试类中仅一个fixture_1被创建了,测试方法test_add_by_class_11中的fixture_1没有再次创建,但是可以复用fixture_1,因为可以获取其返回值。该点涉及到fixture的作用范围。后面会进一步总结。
fixture的优先级
如果一个测试用例同时使用了多个fixture,那么这些fixture谁先执行,谁后执行呢。
大概依据如下原则:
- 不同scope级别的fixture,优先执行高级别的fixture(scope级别由高到低:session-module-class-functin)
- 相同scope级别的fixture,按照使用的顺序(先使用先执行)和依赖关系(被依赖的先执行)来决定执行顺序。
参考如下示例 :
# ./conftest.py
import pytest
@pytest.fixture(scope='session')
def f_1():
print("我是fixture_session")
@pytest.fixture(scope='module')
def f_2():
print("我是fixture_module")
@pytest.fixture(scope='class')
def f_3():
print("我是fixture_class")
@pytest.fixture(scope='function')
def f_temp():
print("我是fixture_function_temp")
@pytest.fixture(scope='function')
def f_4(f_temp):
print("我是fixture_function_4")
@pytest.fixture(scope='function')
def f_5():
print("我是fixture_function_5")
@pytest.fixture(scope='function')
def f_6():
print("我是fixture_function_6")
# ./test_case/test_func.py
import pytest
def test_add_by_func_aaa(f_6, f_5, f_4, f_3, f_2, f_1):
assert 'aaa' == 'aaa'
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item
test_case/test_func.py::test_add_by_func_aaa
我是fixture_session
我是fixture_module
我是fixture_class
我是fixture_function_6
我是fixture_function_5
我是fixture_function_temp
我是fixture_function_4
PASSED
============================== 1 passed in 0.04s ==============================
[Finished in 1.4s]
'''
fixture的作用范围
参考如下原则:
- session级别的fixture在整个测试会话中只需要创建一次,会话中的所有测试用例都可以共享它。
- module级别的fixture在整个测试模块中只需要创建一次,模块中的所有测试用例都可以共享它。
- class级别的fixture在整个测试类中只需要创建一次,测试类中的所有测试用例都可以共享它。
- function级别的fixture在整个测试用例中只需要创建一次,用例中的所有代码都可以共享它。
示例:
# ./conftest.py
import pytest
@pytest.fixture(scope='session')
def f_session():
print("我是fixture_session")
return "我是f_session的返回值"
@pytest.fixture(scope='module')
def f_module():
print("我是fixture_module")
return "我是f_module的返回值"
@pytest.fixture(scope='class')
def f_class():
print("我是fixture_class")
return "我是f_class的返回值"
@pytest.fixture(scope='function')
def f_function():
print("我是fixture_function")
return "我是f_function的返回值"
# ./test_case/test_func.py
import pytest
from func import *
@pytest.mark.usefixtures('f_session')
@pytest.mark.usefixtures('f_module')
@pytest.mark.usefixtures('f_class')
class TestFunc:
# 正常测试用例
def test_add_by_class(self,f_session, f_module, f_class, f_function):
assert add(2,3) == 5
def test_add_by_class_11(self,f_session, f_module, f_class, f_function):
assert add(2,3) == 5
print(f_session)
print(f_module)
print(f_class)
print(f_function)
def test_add_by_func_aaa(f_session, f_module, f_function):
assert 'aaa' == 'aaa'
print(f_session)
print(f_module)
print(f_function)
# ./test_case/test_func_0001.py
def test_fixture(f_session, f_module, f_function):
print(f_session)
print(f_module)
print(f_function)
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 4 items
test_case/test_func.py::TestFunc::test_add_by_class 我是fixture_session
我是fixture_module
我是fixture_class
我是fixture_function
PASSED
test_case/test_func.py::TestFunc::test_add_by_class_11 我是fixture_function
我是f_session的返回值
我是f_module的返回值
我是f_class的返回值
我是f_function的返回值
PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_function
我是f_session的返回值
我是f_module的返回值
我是f_function的返回值
PASSED
test_case/test_func_0001.py::test_fixture 我是fixture_module
我是fixture_function
我是f_session的返回值
我是f_module的返回值
我是f_function的返回值
PASSED
============================== 4 passed in 0.05s ==============================
[Finished in 1.4s]
'''
从以上示例看出,
在test_func.py测试模块中,虽然测试类、测试类中测试方法、测试函数都使用了f_session, f_module, f_class, f_function这几个fixture。但f_session、f_module、f_class只创建了一次,测试模块中的测试用例就可以共享该fixture。而f_function这个fixture在每个测试用例都创建了。
在test_func_0001.py测试模块中,虽然测试用例使用了f_session, f_module, f_function三个fixture,但是只有 f_module, f_function被创建了,但可以共享f_session。
fixture的autouse参数说明
当autouse=True时,在scope指定的范围内,fixture将被强制执行,而无需通过名字使用。
不建议大面积使用该参数,仅在某种需要强制执行fixture的场景中使用。比如web自动化中每个测试用例执行完成后都需要销毁浏览器,就可以使用autouse=True的fixture。
示例:
# ./conftest.py
import pytest
@pytest.fixture(scope='function', autouse=True)
def f_function():
print("我是fixture_function")
return "我是f_function的返回值"
# ./test_case/test_func.py
import pytest
from func import *
class TestFunc:
# 正常测试用例
def test_add_by_class(self):
assert add(2,3) == 5
def test_add_by_class_11(self):
assert add(2,3) == 5
def test_add_by_func_aaa():
assert 'aaa' == 'aaa'
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 3 items
test_case/test_func.py::TestFunc::test_add_by_class 我是fixture_function
PASSED
test_case/test_func.py::TestFunc::test_add_by_class_11 我是fixture_function
PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_function
PASSED
============================== 3 passed in 0.05s ==============================
[Finished in 1.4s]
fixture的重命名
测试用例通过fixture的名字使用fixture。fixture默认名字就是fixture函数名。可以通过name参数指定fixture名字。
示例:
# ./conftest.py
import pytest
@pytest.fixture(scope='function', name="a_renamed_fixture")
def f_function():
print("我是fixture_function")
return "我是f_function的返回值"
# ./test_case/test_func.py
import pytest
from func import *
class TestFunc:
# 正常测试用例
def test_add_by_class(self):
assert add(2,3) == 5
def test_add_by_class_11(self):
assert add(2,3) == 5
def test_add_by_func_aaa(a_renamed_fixture):
assert 'aaa' == 'aaa'
print(a_renamed_fixture)
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 3 items
test_case/test_func.py::TestFunc::test_add_by_class PASSED
test_case/test_func.py::TestFunc::test_add_by_class_11 PASSED
test_case/test_func.py::test_add_by_func_aaa 我是fixture_function
我是f_function的返回值
PASSED
============================== 3 passed in 0.05s ==============================
[Finished in 1.4s]
'''
fixture在测试用例结束后清除、恢复等行为
前面的示例,都是在说测试用例的前置操作,即测试用例执行前,fixture做了什么。那么怎样实现测试用例执行完毕后,使用fiixture做点什么呢。
很简单,只需要一个yield关键字。
示例:
# ./conftest.py
import pytest
@pytest.fixture(scope='function', name="a_renamed_fixture")
def f_function():
yield
print("我是fixture_function,来执行请理恢复任务")
# ./test_case/test_func.py
import pytest
def test_add_by_func_aaa(a_renamed_fixture):
assert 'aaa' == 'aaa'
print("测试用例开始执行")
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item
test_case/test_func.py::test_add_by_func_aaa 测试用例开始执行
PASSED我是fixture_function,来执行请理恢复任务
============================== 1 passed in 0.04s ==============================
[Finished in 1.4s]
'''
为了更清晰些,我们再看看一个没有yileld的示例:
# ./conftest.py
import pytest
@pytest.fixture(scope='function', name="a_renamed_fixture")
def f_function():
#yield
print("我是fixture_function,来执行请理恢复任务")
# ./test_case/test_func.py
import pytest
def test_add_by_func_aaa(a_renamed_fixture):
assert 'aaa' == 'aaa'
print("测试用例开始执行")
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item
test_case/test_func.py::test_add_by_func_aaa 我是fixture_function,来执行请理恢复任务
测试用例开始执行
PASSED
============================== 1 passed in 0.04s ==============================
[Finished in 1.4s]
'''
fixture调用fixture
pytest中测试用例会请求fixture对象。
但由于fixture对象的模块化属性,使得fixture对象请求调用fixture对象成为可能。
调用原则参考fixture的优先级原则:
即 “ 不同scope级别的fixture,优先执行高级别的fixture(scope级别由高到低:session-module-class-functin)”
根据这以原则,可以明白,低优先级的fixture可以调用高优先级的fixture。但高优先级的fixture不可以调用低优先级的fixture。
因为根据“依赖关系(被依赖的先执行)”,被调用(即被依赖)的fixture(低级别)将先执行,这时就违背了高级别的fixture优先执行的原则。
正确示例:低优先级的fixture调用高优先级的fixture
# ./conftest.py
import pytest
@pytest.fixture(scope='function')
def f_function(f_session):
print("我是fixture_function")
print(f_session)
return "FFFunc"
@pytest.fixture(scope='session')
def f_session():
print("我是fixture_session")
#print(f_function)
return "SSSess"
# ./test_case/test_func.py
import pytest
def test_add_by_func_aaa(f_function):
assert 'aaa' == 'aaa'
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item
test_case/test_func.py::test_add_by_func_aaa 我是fixture_session
我是fixture_function
SSSess
PASSED
============================== 1 passed in 0.03s ==============================
[Finished in 1.3s]
'''
错误示例:高优先级的fixture调用低优先级的fixture。结果为直接报错。
# ./conftest.py
import pytest
@pytest.fixture(scope='function')
def f_function():
print("我是fixture_function")
#print(f_session)
return "FFFunc"
@pytest.fixture(scope='session')
def f_session(f_function):
print("我是fixture_session")
print(f_function)
return "SSSess"
# ./test_case/test_func.py
import pytest
def test_add_by_func_aaa(f_session):
assert 'aaa' == 'aaa'
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item
test_case/test_func.py::test_add_by_func_aaa ERROR
=================================== ERRORS ====================================
___________________ ERROR at setup of test_add_by_func_aaa ____________________
ScopeMismatch: You tried to access the 'function' scoped fixture 'f_function' with a 'session' scoped request object, involved factories
conftest.py:14: def f_session(f_function)
conftest.py:6: def f_function()
============================== 1 error in 0.04s ===============================
[Finished in 1.3s]
'''
利用fixture传递参数
由于fixture本身也是个函数对象,所以有返回值。
fixture传底参数其实就是依靠返回值传递参数。
比如,web自动化中,URL就可以通过fixture传递给每个测试用例。
示例:
# ./conftest.py
import pytest
url = r"https://www.baidu.com"
@pytest.fixture(scope='function')
def f_function():
return url
# ./test_case/test_func.py
import pytest
def test_add_by_func_aaa(f_function):
print(f_function)
# ./run_test.py
import pytest
if __name__ == '__main__':
pytest.main(['-v','-s'])
'''
============================= test session starts =============================
platform win32 -- Python 3.7.0, pytest-5.3.4, py-1.8.1, pluggy-0.13.1 -- D:\Python3.7\python.exe
cachedir: .pytest_cache
rootdir: D:\Python3.7\project\pytest, inifile: pytest.ini
plugins: allure-pytest-2.8.9, rerunfailures-8.0
collecting ... collected 1 item
test_case/test_func.py::test_add_by_func_aaa https://www.baidu.com
PASSED
============================== 1 passed in 0.04s ==============================
[Finished in 1.4s]
'''
最后
以上,除了fixture参数化,就是常用的fixture特性。如果描述有误,请指出。