pytest-Fixture
1. 简介
fixture
是在测试函数运行前后,由pytest 执行的外壳函数。fixture 可以定制,以满足不同的测试需求,可定制包括传入测试中的数据集、配置测试前后的setup\teardown、为测试提供批量数据源,等等。
2. 示例
先来举个🌰
import pytest
@pytest.fixture()
def some_data():
return 22
def test_some_data(some_data):
assert some_data == 22
运行结果
$ pytest test_fixtures.py::test_some_data
================================================ test session starts =====================================
platform darwin -- Python 3.7.8, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: xxx
plugins: html-3.0.0, metadata-1.11.0, asyncio-0.14.0, repeat-0.9.1
collected 1 item
test_fixtures.py . [100%]
================================================ 1 passed in 0.02s ======================================
从上面的例子可以看出,装饰器 @pytest.fixture()
是用来声明一个函数为一个fixture。如果测试函数的参数列表中包含fixture 名,那pytest 在执行测试的时候就会检测到,并在执行测试函数之前运行fixture。
3. 全局范围内共享fixture
fixture 可以放在单独的测试文件里,但是如果你希望这是一个公用的fixture ,可以在公共目录下新建一个 conftest.py
文件,把需要公共使用的 fixture 编写在这个文件里面,这样在公共目录下的测试case都可以共享其中的fixture。
尽管 conftest.py
格式看起来是Python模块,但是它不能被在测试代码中被导入。 import conftest
这样的语法是不允许出现的。因为 conftestp.py
是被视作pytest 的一个本地插件使用的。
4. 使用详情
4.1 执行配置和销毁逻辑
使用fixture的好处可以省去setup\teardown的步骤,举个连接数据库的例子。
import pytest
# 伪代码
@pytest.fixture()
def tasks_db():
# 首先测试开始前连接数据库
连接数据库的代码...
# 返回测试代码执行测试
yield
# 测试结束后断开连接
断开连接数据库的代码
yield
之前的代码可以视作 **setup **的过程,而 yield
会返回测试代码执行正常的测试,在测试代码执行完毕后,接着会继续执行 yield
之后的代码,视作 teardown 的过程。
4.2 回溯fixture执行过程
在上面的使用fixture 进行测试的例子中,我们都看不到fixture 的执行过程
但是有的时候我们又希望在执行过程中看到fixture 的详细信息,这时候需要使用 --setup-show
参数了。
展示的信息正如我们的预期一样,我们的测试夹在SETUP 和 TEARDOWN 中间。S 和 F 代表的 fixture 的作用范围,下面来介绍 fixture 的作用范围。
4.3 fixture作用范围
在创建 fixture 时,有一个 scope
的可选参数,用于控制 fixture 执行配置和销毁逻辑的频率(范围)。
这个参数有四个可选值
- function 函数级别(默认)
- class 类级别
- module 模块级别
- session 会话级别
下面来介绍各个级别的参数
scope='function'
函数级别的fixture ,每个测试函数只需要运行一次,配置的代码在测试运行前执行,销毁代码在测试运行后执行。
scope='class'
类级别的fixture ,每个测试类只需要运行一次,无论测试类中有多少个测试方法,都会共享这个fixture
scope='module'
模块级别的fixture ,每个模块都只需要运行一次,无论模块中有多少个测试函数、类、或其他 fixture 都可以共享这个模块级别的 fixture
scope='session'
会话级别的fixture , 每次会话只运行一次,一次pytest 会话中的所有内容都共享这个fixture
import pytest
@pytest.fixture(scope='function')
def func_scope():
"""A function scope fixture."""
@pytest.fixture(scope='module')
def mod_scope():
"""A module scope fixture."""
@pytest.fixture(scope='session')
def sess_scope():
"""A session scope fixture."""
@pytest.fixture(scope='class')
def class_scope():
"""A class scope fixture."""
def test_1(sess_scope, mod_scope, func_scope):
"""Test using session, module, and function scope fixtures."""
def test_2(sess_scope, mod_scope, func_scope):
"""Demo is more fun with multiple tests."""
@pytest.mark.usefixtures('class_scope')
class TestSomething():
"""Demo class scope fixtures."""
def test_3(self):
"""Test using a class scope fixture."""
def test_4(self):
"""Again, multiple tests are more fun."""
下面来看下运行效果,使用 --setup-show
参数来查看每个fixture 的执行过程
$ pytest --setup-show test_scope.py
================================= test session starts ===============================================================================
platform darwin -- Python 3.7.8, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: xxx
plugins: html-3.0.0, metadata-1.11.0, asyncio-0.14.0, repeat-0.9.1
collected 4 items
test_scope.py
SETUP S sess_scope
SETUP M mod_scope
SETUP F func_scope
test_scope.py::test_1 (fixtures used: func_scope, mod_scope, sess_scope).
TEARDOWN F func_scope
SETUP F func_scope
test_scope.py::test_2 (fixtures used: func_scope, mod_scope, sess_scope).
TEARDOWN F func_scope
SETUP C class_scope
test_scope.py::TestSomething::test_3 (fixtures used: class_scope).
test_scope.py::TestSomething::test_4 (fixtures used: class_scope).
TEARDOWN C class_scope
TEARDOWN M mod_scope
TEARDOWN S sess_scope
================================= 4 passed in 0.05s ================================================================================
从上面的测试结果中不难看出,出现了 S M F 等级别的fixture。
是因为,作用范围虽然是由 fixture 自身定义,但还是要强调 scope 参数是在定义 fixture 时定义的,而不是在调用 fixture 时定义的,因此使用 fixture 的测试函数是无法改变 fixture 的作用范围的。
fixture 只能使用同级别的 fixture,或者比自己更高级别的 fixture。如函数级别的 fixture 可以使用同级别的 fixture ,也可以使用类级别、会话级别等。但是不能反过来。
4.4 使用usefixtures指定fixture
上面的例子中(除4.3)都是直接在测试函数的参数列表里指定使用的fixture,还有一种使用方法,就是用 @pytest.mark.usefixtures('fixture1, fixture2')
这样的装饰器来指定使用的 fixture。
import pytest
@pytest.mark.usefixtures('class_scope')
class TestSomething():
"""Demo class scope fixtures."""
def test_3(self):
"""Test using a class scope fixture."""
def test_4(self):
"""Again, multiple tests are more fun."""
但是两者还是有区别的,usefixtures是无法使用fixture 的返回值的,在参数列表中指定 fixture 是可以使用 fixture 的返回值的。
4.5 autouse选项
我们可以通过 autouse=Ture
选项来使作用域内的测试函数都执行该 fixture 。一般情况下,需要运行多次且不依赖任何系统状态/外部数据的测试使用较多。
import pytest
import time
@pytest.fixture(autouse=True, scope='session')
def footer_session_scope():
yield
now = time.time()
print('--')
print('finished : {}'.format(time.strftime('%d %b %X', time.localtime(now))))
print('-----------------')
@pytest.fixture(autouse=True)
def footer_function_scope():
start = time.time()
yield
stop = time.time()
delta = stop - start
print('\ntest duration : {:0.3} seconds'.format(delta))
def test_1():
time.sleep(1.25)
def test_2():
time.sleep(2.5)
上面这段代码,我们希望在每次测试会话结束后都打印结束的日期和时间,并打印出每个测试所使用的的时间。
下面为测试结果
$ pytest -vs test_autouse.py
============================= test session starts ===============================================================================
platform darwin -- Python 3.7.8, pytest-6.1.2, py-1.9.0, pluggy-0.13.1 -- /Library/Frameworks/Python.framework/Versions/3.7/bin/python3.7
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Darwin-20.1.0-x86_64-i386-64bit', 'Packages': {'pytest': '6.1.2', 'py': '1.9.0', 'pluggy': '0.13.1'}, 'Plugins': {'html': '3.0.0', 'metadata': '1.11.0', 'asyncio': '0.14.0', 'repeat': '0.9.1'}, 'JAVA_HOME': '/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home'}
rootdir: xxx
plugins: html-3.0.0, metadata-1.11.0, asyncio-0.14.0, repeat-0.9.1
collected 2 items
test_autouse.py::test_1 PASSED
test duration : 1.25 seconds
test_autouse.py::test_2 PASSED
test duration : 2.5 seconds
--
finished : 01 Mar 16:05:11
-----------------
==================================== 2 passed in 3.81s ================================================================================
但是除非你可以肯定测试代码不依赖任何,也不需要任何数据的情况下,否则尽量少使用这种方式。
4.6 为fixture重命名
fixture 的名字有的时候可能会很长,我们目前使用fixture 都是使用定义时候的函数名字,但是当这个名字很长但是有意义你不想改变的时候,我们可以为该fixture 重命名,使其使用起来更方便
重命名的方式也很简单,就是在定义的时候加上参数 name
: @pytest.fixture(name='xxx')
import pytest
@pytest.fixture(name='lue')
def ultimate_answer_to_life_the_universe_and_everything():
return 42
def test_everything(lue):
assert lue == 42
这样我们就以一个简短明了的名字使用了fixture ,但是该fixture 还是保持的它那个有意义且很明确的名字。在测试执行的时候也会变成这简短明了的名字
$ pytest --setup-show test_rename_fixture.py
================================= test session starts ===============================================================================
platform darwin -- Python 3.7.8, pytest-6.1.2, py-1.9.0, pluggy-0.13.1
rootdir: xxx
plugins: html-3.0.0, metadata-1.11.0, asyncio-0.14.0, repeat-0.9.1
collected 1 item
test_rename_fixture.py
SETUP F lue
test_rename_fixture.py::test_everything (fixtures used: lue).
TEARDOWN F lue
================================== 1 passed in 0.03s ================================================================================
当我们想查看 lue
来自哪里的时候,我们可以使用 --fixtures
参数来查看,由于输出内容很多,下面展示需要的简短的信息
$ pytest --fixtures test_rename_fixture.py
...
------------------------------- fixtures defined from test_rename_fixture --------------------------------------------------------------------
lue
Return ultimate answer.
================================== no tests ran in 0.03s ==============================================================================