pytest 框架还提供了 Fixture 夹具,使我们能够定义一个通用的设置步骤,该步骤可以反复使用,就像使用普通函数一样。不同的测试用例可以请求同一个 Fixture 进行同样的步骤处理,然后 pytest 会根据该 Fixture 为每个测试测试用例提供各自的结果数据。

这对于确保测试用例不会相互影响非常有用,我们可以使用此系统确保每个测试都获得自己的新数据,并从干净的状态开始,以便提供一致、可重复的结果。比如用户模块的各个测试用例,都可以被查询用户信息的 Fixture 夹具所装饰,或者运行每个测试用例前清理数据库数据。

测试用例使用 Fixture

# \test\test_mod1.py
import pytest

@pytest.fixture
def fixture_01():
    return [1, 2, 3]

def test_01(fixture_01):
    assert fixture_01 == [1, 2, 3], "测试失败"


def test_02(fixture_01):
    assert fixture_01 == [1, 2], "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
collected 4 items                                                                                                                                                

Config::test_01 PASSED                                                                                                                                    [ 25%] 
Config::test_02 FAILED                                                                                                                                    [ 50%]
Config::testClass::test_03 PASSED                                                                                                                         [ 75%] 
Config::testClass::test_04 FAILED                                                                                                                         [100%] 

=================================================================== short test summary info ==================================================================== 
FAILED Config::test_02 - AssertionError: 测试失败
FAILED Config::testClass::test_04 - AssertionError: 测试失败
================================================================= 2 failed, 2 passed in 0.03s ================================================================== 

测试用例使用多个 Fixture

# \test\test_mod1.py
import pytest

@pytest.fixture
def fixture_01():
    return [1, 2, 3]

@pytest.fixture
def fixture_02():
    return [1, 2, 3]

def test_01(fixture_01, fixture_02):
    assert fixture_01 == fixture_02, "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
collected 1 item                                                                                                                                                 

Config::test_01 PASSED                                                                                                                                    [100%] 

====================================================================== 1 passed in 0.01s ======================================================================= 

Fixture 函数可以被多个测试用例复用,但是要注意执行顺序是从左到右:

# \test\test_mod1.py
import pytest

data = {"a": 2, "b": 2}

# data["a"]
@pytest.fixture
def fixture_a_01():
    data["a"] *= 10


@pytest.fixture
def fixture_a_02():
    data["a"] += 10

def test_a(fixture_a_01, fixture_a_02):
    assert data["a"] == 30, "测试失败"

# data["b"]
@pytest.fixture
def fixture_b_01():
    data["b"] *= 10


@pytest.fixture
def fixture_b_02():
    data["b"] += 10

def test_b(fixture_b_02, fixture_b_01):
    assert data["b"] == 120, "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
collected 2 items                                                                                                                                               

Config::test_a PASSED                                                                                                                                     [ 50%] 
Config::test_b PASSED                                                                                                                                     [100%] 

====================================================================== 2 passed in 0.01s ======================================================================= 

执行顺序不一样,往往会导致结果不同。

Fixture 使用其它 Fixture

# \test\test_mod1.py
import pytest

@pytest.fixture
def fixture_01():
    return [1, 2, 3]

@pytest.fixture
def fixture_02(fixture_01):
    return fixture_01

def test_01(fixture_02):
    assert fixture_02 == [1, 2, 3], "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
collected 1 item                                                                                                                                                 

Config::test_01 PASSED                                                                                                                                    [100%] 

====================================================================== 1 passed in 0.01s ======================================================================= 

Fixture 函数作为工厂

# \test\test_mod1.py
import pytest


@pytest.fixture
def make_customer_record():
    def make_customer_record(name):
        return {"name": name, "orders": []}

    return make_customer_record


def test_customer_records(make_customer_record):
    customer_1 = make_customer_record("Lisa")
    assert customer_1["name"] == "Lisa"
    customer_2 = make_customer_record("Mike")
    assert customer_2["name"] == "Mike"
    customer_3 = make_customer_record("Meredith")
    assert customer_3["name"] == "Meredith"

运行结果:

===================================================================== test session starts ======================================================================
collected 1 item                                                                                                                                                 

Config::test_customer_records PASSED                                                                                                                      [100%] 

====================================================================== 1 passed in 0.01s ======================================================================= 

Fixture 的作用域

Fixtures 可以设置为 5 个不同的作用域,分别是:

  • session:整个测试会话(即所有模块)只会初始化一次 Fixtures 。
  • package:包(即目录)中只会初始化一次 Fixtures 。
  • module:模块中只会初始化一次 Fixtures 。
  • class:测试组中只会初始化一次 Fixtures 。
  • function:每个测试用例都会初始化一次 Fixtures 。
  1. 设置 Fixtures 作用域为 function
# \test\test_mod1.py
import pytest


@pytest.fixture(scope="function")
def my_list():
    return [1, 2, 3]

def test_one(my_list):
    my_list.append(4)
    assert my_list == [1, 2, 3, 4]


def test_two(my_list):
    assert my_list == [1, 2, 3]


def test_three(my_list):
    assert my_list == [1, 2, 3]

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 3 items                                                                                                                                                

Config::test_one PASSED                                                                                                                                   [ 33%] 
Config::test_two PASSED                                                                                                                                   [ 66%] 
Config::test_three PASSED                                                                                                                                 [100%] 

====================================================================== 3 passed in 0.01s ======================================================================= 

每一个测试用例请求 Fixtures:my_list ,都会初始化 my_list 的值,每个测试用例接收的值都是 [1, 2, 3] 。

  1. 设置 Fixture 作用域为 module
# \test\test_mod1.py
import pytest

@pytest.fixture(scope="module")
def my_list():
    return [1, 2, 3]

def test_one(my_list):
    my_list.append(4)
    assert my_list == [1, 2, 3, 4]


def test_two(my_list):
    assert my_list == [1, 2, 3]


def test_three(my_list):
    assert my_list == [1, 2, 3]

运行结果:

===================================================================== test session starts ======================================================================
configfile: pytest.ini
plugins: dependency-0.6.0, order-1.3.0
collected 3 items                                                                                                                                                

Config::test_one PASSED                                                                                                                                   [ 33%] 
Config::test_two FAILED                                                                                                                                   [ 66%]
Config::test_three FAILED                                                                                                                                 [100%] 

=================================================================== short test summary info ==================================================================== 
FAILED Config::test_two - AssertionError: assert [1, 2, 3, 4] == [1, 2, 3]
FAILED Config::test_three - AssertionError: assert [1, 2, 3, 4] == [1, 2, 3]
================================================================= 2 failed, 1 passed in 0.04s ================================================================== 

由于 Fixtures:my_list 在整个模块中只会初始化一次,执行 test_one 后,my_list 的值:[1, 2, 3, 4] 被缓存,后续执行 test_two、test_three 时,my_list 的值都是缓存值:[1, 2, 3, 4] 。

其它作用域的逻辑与 module 一致,只是作用域不同,这里不再赘述。

在 Fixture 使用 yield

在 Fixture 里,可以使用 yield 代替 return,yield 相比 return 多了执行 yield 后代码的功能。

逻辑是这样的:运行测试用例时,首先执行 Fixture 函数;Fixture 函数内部执行到 yield 时,会将数据返回给测试用例;最后测试用例执行结束后,在运行 yield 后面的代码。

我们先设想一个测试流程:

  1. 新建用户
  2. 查询数据库,是否新建成功
  3. 清理新建的用户

利用 yield,我们可以这样写:

# \test\test_mod1.py
import pytest

# 使用字典模拟数据库
user = {}
@pytest.fixture()
def add_user():
    user['user1'] = 'JZY'

@pytest.fixture()
def select_user():
    yield user['user1']
    user.pop('user1')

def test_one(add_user, select_user):
    assert select_user == 'JZY', "测试失败"

def test_two():
    assert user == {'user1': 'JZY'}, "测试失败"

运行结果:

===================================================================== test session starts ======================================================================
collected 2 items                                                                                                                                                

Config::test_one PASSED                                                                                                                                   [ 50%] 
Config::test_two FAILED                                                                                                                                   [100%]

=================================================================== short test summary info ==================================================================== 
FAILED Config::test_two - AssertionError: 测试失败
================================================================= 1 failed, 1 passed in 0.03s ================================================================== 

yield 语句不仅返回值,还在测试完成后执行后续的代码。

多个Fixture 的 yield 以先进后出(从右到左)的顺序执行。比如:

import pytest


def test_bar(fix_w_yield1, fix_w_yield2):
    print("test_bar")


@pytest.fixture
def fix_w_yield1():
    yield
    print("after_yield_1")


@pytest.fixture
def fix_w_yield2():
    yield
    print("after_yield_2")

运行结果:

============================= test session starts =============================
collecting ... collected 1 item

test/test_a.py::test_bar PASSED                                          [100%]
after_yield_2
after_yield_1

============================== 1 passed in 0.01s ==============================

先执行 fix_w_yield2 的 yield,后执行 fix_w_yield1 的 yield。

传递参数给 Fixture

Mark 标记传参

# \test\test_mod1.py
import pytest

@pytest.fixture
def fixt(request):
    marker = request.node.get_closest_marker("user")
    if marker is None:
        # 未获取到标记,则返回None
        data = None
    else:
        # 获取到标记,则返回标记数据
        data = marker.args[0]

    return data

# 已在 pytest.ini 中配置了 user 标记
@pytest.mark.user(42)
def test_fixt(fixt):
    assert fixt == 42

python .\app.py ,运行结果:

===================================================================== test session starts ======================================================================
collected 1 item                                                                                                                                                 

Config::test_fixt PASSED                                                                                                                                  [100%] 

====================================================================== 1 passed in 0.01s ======================================================================= 

定义 Fixture 时,参数化数据

import pytest

# 可以通过 pytest.param("a good person", marks=pytest.mark.user) 对数据进行 mark 标记
@pytest.fixture(params=["JZY", "one man", pytest.param("a good person", marks=pytest.mark.user)])
def fixt(request):
    """
    通过 request.param 获取参数值
    """
    return request.param

def test_fixt(fixt):
    assert (fixt == "JZY") or (fixt == "one man") or (fixt == "a good person")

运行结果:

===================================================================== test session starts ======================================================================
collected 3 items                                                                                                                                                

Config::test_fixt[JZY] PASSED                                                                                                                             [ 33%] 
Config::test_fixt[one man] PASSED                                                                                                                         [ 66%] 
Config::test_fixt[a good person] PASSED                                                                                                                   [100%] 

====================================================================== 3 passed in 0.01s ======================================================================= 

应用 conftest.py 文件

conftest.py 是 pytest 框架中一个非常重要的特殊文件,它用于定义共享的 fixture、钩子函数(hooks)和配置,这些定义对同一目录及其所有子目录下的测试文件都是可见的。

可以把它理解为一个配置和共享中心,用于集中管理测试所需的公共设置、数据和工具。

pytest 会自动发现 conftest.py 中的 fixture 函数,使用时无需显式导入。

假设有一个测试目录如下:

├── tests/                    # 测试用例目录
│   ├── conftest.py           # 1
│   ├── __init__.py           # 包初始化文件
│   └── login/				  # login 模块测试目录
│       ├── conftest.py		  # 2
│       └── test_login.py     # 测试 login 模块的测试用例

注释为 1 的 conftest.py 中 fixture 函数,可以被 tests/ 目录及其子目录下的所有测试用例使用。假设 conftest.py 中有名为 clear 的 fixture 函数,tests/ 目录及其子目录下的所有测试用例,都无需显式导入而直接使用。

注释为 2 的 conftest.py 中 fixture 函数,可以被 login/ 目录及其子目录下的所有测试用例使用。假设 conftest.py 中有名为 clear 的 fixture 函数,login/ 目录及其子目录下的所有测试用例,都无需显式导入而直接使用。非 login/ 目录及其子目录下的所有测试用例,不能不显式导入而直接使用。

自动使用 Fixture

autouse 设置为 true,Pytest 会自动使用 Fixture,常用于初始化操作。比如执行测试前,检查某个条件是否达到要求(如环境变量、配置文件、服务可达性等),如果不满足,直接退出。比如:

# conftest.py
import pytest
import os

@pytest.fixture(autouse=True, scope="session")
def check_test_condition():
    # 示例:检查环境变量是否设置
    env = os.getenv("TEST_ENV")
    if not env:
        pytest.exit("❌ 环境变量 TEST_ENV 未设置,终止所有测试!", returncode=1)

    if env not in ["dev", "staging"]:
        pytest.exit(f"❌ 不支持的环境: {env},仅支持 dev 或 staging", returncode=1)

    print(f"✅ 环境检查通过:TEST_ENV={env}")

还有一些 Fixtures 的用法没有在文章中说明,感兴趣的读者可以访问 How to use fixtures 了解,里面有很详细的介绍。


THEEND



© 转载需要保留原始链接,未经明确许可,禁止商业使用。CC BY-NC-ND 4.0