Skip to content

什么是固件

固件(Fixture)是一些函数,pytest 会在执行测试函数之前(或之后)加载运行它们。

我们可以利用固件做任何事情,其中最常见的可能就是数据库的初始连接和最后关闭操作。

Pytest 使用 pytest.fixture() 定义固件,下面是最简单的固件,只返回北京邮编:

python
# 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 之后的代码属于后处理,将在测试完成后执行。

以下测试模拟数据库查询,使用固件来模拟数据库的连接关闭:

python
# 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 阻止消息被吞:

shell
$ 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 选项:

shell
$ 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

python
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

最简单使用固件方式是作为测试函数参数:

python
# test_03_scope.py

def test_multi_scope(sess_scope, mod_scope, func_scope):
    pass

执行结果如下,可以清楚看到各固件的作用域和执行顺序:

shell
$ 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 作用域):

python
# 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))

注意下面的两个测试函数并都没有显式使用固件:

python
def test_1():
    time.sleep(1)


def test_2():
    time.sleep(2)

执行测试可看到,固件自动执行并完成计时任务:

shell
$ 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 选项指定名称:

python
# test_05_rename.py

@pytest.fixture(name='age')
def calculate_average_age():
    return 28


def test_age(age):
    assert age == 28

参数化

在“第二部分 测试函数”中,介绍了函数的参数化测试:

python
@pytest.mark.parametrize(
    'passwd',
    ['123456', 'abcdefdfs', 'as52345fasdf4']
)
def test_passwd_length(passwd):
    assert len(passwd) >= 8

因为固件也是函数,我们同样可以对固件进行参数化。在什么情况下需要对固件参数化?

假设现在有一批 API 需要测试对不同数据库的支持情况(对所有数据库进行相同操作),最简单的方法就是针对每个数据库编写一个测试用例,但这包含大量重复代码,如数据库的连接、关闭,查询等。

进一步,可以使用固件抽离出数据库的通用操作,每个 API 都能复用这些数据库固件,同时可维护性也得到提升。

更进一步,可以继续将这些固件合并为一个,而通过参数控制连接到不同的数据库。这就需要使用固件参数化来实现。固件参数化需要使用 pytest 内置的固件 request,并通过 request.param 获取参数。

shell
# 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

执行结果:

shell
$ 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() 创建临时文件(或者使用创建的目录)。

python
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

python
@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 ):

python
# 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 获取命令行参数:

python
# test_config.py

def test_option1(pytestconfig):
    print('host: %s' % pytestconfig.getoption('host'))
    print('port: %s' % pytestconfig.getoption('port'))

pytestconfig 其实是 request.config 的快捷方式,所以也可以自定义固件实现命令行参数读取。

python
# 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'))

执行结果:

shell
$ 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 用于捕获 stdoutstderr 的内容,并临时关闭系统输出。

python
# 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'