什么是固件
固件(Fixture)是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们。
我们可以利用固件做任何事情,其中最常见的可能就是数据库的初始连接和最后关闭操作。
Pytest 使用 pytest.fixture()
定义固件,下面是最简单的固件,只返回北京邮编:
# test_01_postcode.py
import pytest
@pytest.fixture()
def postcode():
return '010'
def test_postcode(postcode):
assert postcode == '010'
固件可以直接定义在各测试脚本中,就像上面的例子。更多时候,我们希望一个固件可以在更大程度上复用,这就需要对固件进行集中管理。Pytest 使用文件 conftest.py
集中管理固件。
在复杂的项目中,可以在不同的目录层级定义
conftest.py
,其作用域为其所在的目录和子目录。
注意:不要自己显式调用 conftest.py
,pytest 会自动调用,可以把 conftest 当做插件来理解。
预处理和后处理
很多时候需要在测试前进行预处理(如新建数据库连接),并在测试完成进行清理(关闭数据库连接)。
当有大量重复的这类操作,最佳实践是使用固件来自动化所有预处理和后处理。
Pytest 使用 yield
关键词将固件分为两部分,yield
之前的代码属于预处理,会在测试前执行;yield
之后的代码属于后处理,将在测试完成后执行。
以下测试模拟数据库查询,使用固件来模拟数据库的连接关闭:
# test_02_db.py
import pytest
@pytest.fixture()
def db():
print('Connection successful')
yield
print('Connection closed')
def search_user(user_id):
d = {
'001': 'xiaoming'
}
return d[user_id]
def test_search(db):
print('test_search')
assert search_user('001') == 'xiaoming'
执行时使用 -s
阻止消息被吞:
$ pytest -s test_02_db.py
========================= test session starts ==========================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test_03
plugins: anyio-4.9.0, langsmith-0.3.34
collected 1 item
02_test_db.py Connection successful
test_search
.Connection closed
========================== 1 passed in 0.04s ===========================
可以看到在测试成功的 .
标识前后有数据库的连接和关闭操作。
小技巧
如果想更细的跟踪固件执行,可以使用 --setup-show
选项:
$ pytest --setup-show test_02_db.py
========================= test session starts ==========================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test_03
plugins: anyio-4.9.0, langsmith-0.3.34
collected 1 item
02_test_db.py
SETUP F db
02_test_db.py::test_search (fixtures used: db).
TEARDOWN F db
========================== 1 passed in 0.03s ===========================
作用域
固件的作用是为了抽离出重复的工作和方便复用,为了更精细化控制固件(比如只想对数据库访问测试脚本使用自动连接关闭的固件),pytest 使用作用域来进行指定固件的使用范围。
在定义固件时,通过 scope
参数声明作用域,可选项有:
function
: 函数级,每个测试函数都会执行一次固件;class
: 类级别,每个测试类执行一次,所有方法都可以使用;module
: 模块级,每个模块执行一次,模块内函数和方法都可使用;session
: 会话级,一次测试只执行一次,所有被找到的函数和方法都可用。
默认的作用域为
function
。
import pytest
@pytest.fixture(scope='function')
def func_scope():
pass
@pytest.fixture(scope='module')
def mod_scope():
pass
@pytest.fixture(scope='session')
def sess_scope():
pass
@pytest.fixture(scope='class')
def class_scope():
pass
最简单使用固件方式是作为测试函数参数:
# test_03_scope.py
def test_multi_scope(sess_scope, mod_scope, func_scope):
pass
执行结果如下,可以清楚看到各固件的作用域和执行顺序:
$ pytest --setup-show 03_test_scope.py::test_multi_scope
========================= test session starts ==========================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test_03
plugins: anyio-4.9.0, langsmith-0.3.34
collected 1 item
03_test_scope.py
SETUP S sess_scope
SETUP M mod_scope
SETUP F func_scope
03_test_scope.py::test_multi_scope (fixtures used: func_scope, mod_scope, sess_scope).
TEARDOWN F func_scope
TEARDOWN M mod_scope
TEARDOWN S sess_scope
========================== 1 passed in 0.04s ===========================
对于类使用作用域,需要使用 pytest.mark.usefixtures
(对函数和方法也适用):
自动执行
目前为止,所有固件的使用都是手动指定,或者作为参数,或者使用 usefixtures
。
如果我们想让固件自动执行,可以在定义时指定 autouse
参数。
下面是两个自动计时固件,一个用于统计每个函数运行时间(function
作用域),一个用于计算测试总耗时(session
作用域):
# test_04_auto_use.py
import time
import pytest
DATE_FORMAT = '%Y-%m-%d %H:%M:%S'
@pytest.fixture(scope='session', autouse=True)
def timer_session_scope():
start = time.time()
print('\nstart: {}'.format(time.strftime(DATE_FORMAT, time.localtime(start))))
yield
finished = time.time()
print('finished: {}'.format(time.strftime(DATE_FORMAT, time.localtime(finished))))
print('Total time cost: {:.3f}s'.format(finished - start))
@pytest.fixture(autouse=True)
def timer_function_scope():
start = time.time()
yield
print(' Time cost: {:.3f}s'.format(time.time() - start))
注意下面的两个测试函数并都没有显式使用固件:
def test_1():
time.sleep(1)
def test_2():
time.sleep(2)
执行测试可看到,固件自动执行并完成计时任务:
$ pytest -s test_04_auto_use.py
========================= test session starts ==========================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test_03
plugins: anyio-4.9.0, langsmith-0.3.34
collected 2 items
test_04_auto_use.py
start: 2025-06-03 14:26:54
. Time cost: 1.000s
. Time cost: 2.001s
finished: 2025-06-03 14:26:57
Total time cost: 3.004s
========================== 2 passed in 3.06s ===========================
重命名
固件的名称默认为定义时的函数名,如果不想使用默认,可以通过 name
选项指定名称:
# test_05_rename.py
@pytest.fixture(name='age')
def calculate_average_age():
return 28
def test_age(age):
assert age == 28
参数化
在“第二部分 测试函数”中,介绍了函数的参数化测试:
@pytest.mark.parametrize(
'passwd',
['123456', 'abcdefdfs', 'as52345fasdf4']
)
def test_passwd_length(passwd):
assert len(passwd) >= 8
因为固件也是函数,我们同样可以对固件进行参数化。在什么情况下需要对固件参数化?
假设现在有一批 API 需要测试对不同数据库的支持情况(对所有数据库进行相同操作),最简单的方法就是针对每个数据库编写一个测试用例,但这包含大量重复代码,如数据库的连接、关闭,查询等。
进一步,可以使用固件抽离出数据库的通用操作,每个 API 都能复用这些数据库固件,同时可维护性也得到提升。
更进一步,可以继续将这些固件合并为一个,而通过参数控制连接到不同的数据库。这就需要使用固件参数化来实现。固件参数化需要使用 pytest 内置的固件 request
,并通过 request.param
获取参数。
# filename: test_06_parametrize.py
@pytest.fixture(params=[
('redis', '6379'),
('elasticsearch', '9200')
])
def param(request):
return request.param
@pytest.fixture(autouse=True)
def db(param):
print('\nSucceed to connect %s:%s' % param)
yield
print('\nSucceed to close %s:%s' % param)
def test_api():
assert 1 == 1
执行结果:
$ pytest -s test_06_parametrize.py
========================= test session starts ==========================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test_03
plugins: anyio-4.9.0, langsmith-0.3.34
collected 2 items
test_06_parametrize.py
Succeed to connect redis:6379
.
Succeed to close redis:6379
Succeed to connect elasticsearch:9200
.
Succeed to close elasticsearch:9200
========================== 2 passed in 0.06s ===========================
注解
与函数参数化使用 @pytest.mark.parametrize
不同,固件在定义时使用 params
参数进行参数化。
固件参数化依赖于内置固件 request
及其属性 param
。
内置固件
tmpdir & tmpdir_factory
用于临时文件和目录管理,默认会在测试结束时删除。
tmpdir
只有function
作用域,只能在函数内使用。
使用 tmpdir.mkdir()
创建目临时录,tmpdir.join()
创建临时文件(或者使用创建的目录)。
def test_tmpdir(tmpdir):
a_dir = tmpdir.mkdir('mytmpdir')
a_file = a_dir.join('tmpfile.txt')
a_file.write('hello, pytest!')
assert a_file.read() == 'hello, pytest!'
tmpdir_factory
可以在所有作用域使用,包括function, class, module, session
。
@pytest.fixture(scope='module')
def my_tmpdir_factory(tmpdir_factory):
a_dir = tmpdir_factory.mktemp('mytmpdir')
a_file = a_dir.join('tmpfile.txt')
a_file.write('hello, pytest!')
return a_file
pytestconfig
使用 pytestconfig
,可以很方便的读取命令行参数和配置文件。
下面示例演示命令行参数解析:首先在 conftest.py
中使用函数 pytest_addoption
(特定的 hook function ):
# conftest.py
def pytest_addoption(parser):
parser.addoption('--host', action='store', help='host of db')
parser.addoption('--port', action='store', default='8888', help='port of db')
然后就可以在测试函数中通过 pytestconfig
获取命令行参数:
# test_config.py
def test_option1(pytestconfig):
print('host: %s' % pytestconfig.getoption('host'))
print('port: %s' % pytestconfig.getoption('port'))
pytestconfig
其实是request.config
的快捷方式,所以也可以自定义固件实现命令行参数读取。
# conftest.py
def pytest_addoption(parser):
parser.addoption('--host', action='store', help='host of db')
parser.addoption('--port', action='store', default='8888', help='port of db')
@pytest.fixture
def config(request):
return request.config
# test_config.py
def test_option2(config):
print('host: %s' % config.getoption('host'))
print('port: %s' % config.getoption('port'))
执行结果:
$ pytest -s --host=localhost test_config.py::test_option2
=============================== test session starts ================================
platform win32 -- Python 3.12.9, pytest-8.4.0, pluggy-1.6.0
rootdir: Z:\zhengxin\py_test_03
plugins: anyio-4.9.0, langsmith-0.3.34
collected 1 item
test_config.py host: localhost
port: 8888
.
================================ 1 passed in 0.06s =================================
capsys
capsys
用于捕获 stdout
和 stderr
的内容,并临时关闭系统输出。
# test_capsys.py
import sys
def ping(output):
print('Pong...', file=output)
def test_stdout(capsys):
ping(sys.stdout)
out, err = capsys.readouterr()
assert out == 'Pong...\n'
assert err == ''
def test_stderr(capsys):
ping(sys.stderr)
out, err = capsys.readouterr()
assert out == ''
assert err == 'Pong...\n'