Fixture详解


文章目录


一、先准备环境

官方入门文档推荐使用如下命令安装 pytest:

bash 复制代码
pip install -U pytest
pytest --version

同时,强烈建议在虚拟环境中安装和使用 pytest,以避免依赖冲突(参见 [pytest 官方文档][2])。


二、从一个最小示例理解 fixture

首先创建如下目录结构:

text 复制代码
demo_pytest_fixture/
└─ tests/
   └─ test_basic_fixture.py

文件内容如下:

python 复制代码
# tests/test_basic_fixture.py
import pytest


@pytest.fixture
def user():
    print("\n[fixture] 创建测试用户")
    return {"name": "张三", "role": "tester"}


def test_user_name(user):
    assert user["name"] == "张三"


def test_user_role(user):
    assert user["role"] == "tester"

运行命令:

bash 复制代码
pytest -s

关键理解点:

  1. test_user_name(user) 中的 user 不是普通参数
  2. 它是在"请求"一个名为 user 的 fixture;
  3. pytest 会自动查找 @pytest.fixture def user()
  4. 执行该函数,并将其返回值注入到测试函数中。

这正是 pytest 的核心机制:测试函数通过参数声明所需资源,pytest 根据参数名自动解析并注入对应的 fixture 实例(详见 [pytest 文档][1])。


三、为什么 fixture 比传统的 setup/teardown 更常用?

fixture 的优势不仅在于"初始化",更在于它统一解决了三个关键问题

  1. 前置准备(Setup)
  2. 资源共享(Reuse)
  3. 后置清理(Teardown)

更重要的是,这些能力可以被拆分为多个独立、可复用的小型 fixture。官方将 fixture 定义为"为测试提供可靠且一致上下文的机制",支持通过 returnyield 两种方式提供资源(参见 [pytest 文档][3])。


四、Fixture 执行流程图解

下图展示了 fixture 的完整执行模型(可直接复制到 PlantUML 使用):

其核心逻辑是:

  • pytest 先解析 fixture 的依赖关系;
  • 依次执行依赖项,直到遇到 returnyield
  • 将资源对象传递给测试函数;
  • 若使用 yield,测试结束后会回溯执行 yield 后的清理代码

这正是官方描述的"setup → yield → test → teardown"模型(参见 [pytest 文档][1])。


五、最佳实践:将 fixture 放入 conftest.py

在实际项目中,最常见的方式是将 fixture 定义在 conftest.py

📌 注意:

  • 文件名必须为 conftest.py,不可更改;
  • 无需手动导入,pytest 会自动发现;
  • 作用域为当前目录及其所有子目录;
  • 若多层目录中存在同名 fixture,优先使用最近的 conftest.py

目录结构示例:

text 复制代码
demo_pytest_fixture/
└─ tests/
   ├─ conftest.py
   └─ test_login.py

1)conftest.py

python 复制代码
# tests/conftest.py
import pytest


@pytest.fixture(scope="session")
def base_url():
    print("\n[session] 初始化 base_url")
    return "https://demo.example.com"


@pytest.fixture(scope="function")
def login_user():
    print("\n[function] 准备登录账号")
    return {"username": "admin", "password": "123456"}


@pytest.fixture
def login_session(base_url, login_user):
    print(f"\n[fixture] 模拟登录: {login_user['username']} -> {base_url}")
    token = f"token-{login_user['username']}"
    yield {
        "token": token,
        "user": login_user,
        "base_url": base_url
    }
    print("\n[teardown] 退出登录,清理会话")

2)test_login.py

python 复制代码
# tests/test_login.py
def test_login_success(login_session):
    assert login_session["token"].startswith("token-")


def test_login_user_is_admin(login_session):
    assert login_session["user"]["username"] == "admin"

运行后你会发现:

  • test_login.py 无需 import login_session
  • pytest 自动解析依赖链:login_sessionbase_url + login_user
  • 所有 fixture 按需自动注入。

这就是 conftest.py 的强大之处:提供目录级共享资源,且天然支持依赖注入(参见 [pytest 文档][4])。


六、深入理解 fixture 的作用域(scope)

scope 决定了 fixture 的生命周期和复用范围。

pytest 支持以下五种作用域(按生命周期从长到短):

Scope 生命周期
session 整个测试会话期间仅初始化一次
package 当前包内所有测试共享
module 单个 .py 文件内共享
class 单个测试类内共享
function 默认,每个测试函数独享

⚠️ 注意:高作用域 fixture 优先于低作用域执行;同一作用域内,按依赖顺序执行(参见 [pytest 文档][5])。

作用域层级关系图(PlantUML)

原则

  • 越靠左(如 session),复用性越强,但隔离性越弱;
  • 越靠右(如 function),隔离性越好,但开销更大。

七、作用域实战示例

python 复制代码
# tests/test_scope.py
import pytest


@pytest.fixture(scope="session")
def sess():
    print("\nSETUP session")
    yield "session"
    print("\nTEARDOWN session")


@pytest.fixture(scope="module")
def mod():
    print("\nSETUP module")
    yield "module"
    print("\nTEARDOWN module")


@pytest.fixture(scope="class")
def cls():
    print("\nSETUP class")
    yield "class"
    print("\nTEARDOWN class")


@pytest.fixture(scope="function")
def func():
    print("\nSETUP function")
    yield "function"
    print("\nTEARDOWN function")


class TestDemo:
    def test_a(self, sess, mod, cls, func):
        assert [sess, mod, cls, func] == ["session", "module", "class", "function"]

    def test_b(self, sess, mod, cls, func):
        assert True

典型输出:

text 复制代码
SETUP session
SETUP module
SETUP class
SETUP function
test_a
TEARDOWN function
SETUP function
test_b
TEARDOWN function
TEARDOWN class
TEARDOWN module
TEARDOWN session

从中可清晰看出:

  • sessionmoduleclass 各只初始化一次;
  • function 每次测试都重新创建;
  • 清理顺序与初始化顺序相反(LIFO)。

这完全符合官方对 fixture 生命周期的定义(参见 [pytest 文档][5])。


八、yield 的真正价值:优雅的后置清理

yield 不仅用于返回资源,更关键的是提供可靠的清理机制

示例:

python 复制代码
# tests/test_yield_demo.py
import pytest


@pytest.fixture
def open_file():
    print("\n[setup] 打开文件")
    f = open("temp_demo.txt", "w", encoding="utf-8")
    yield f
    print("\n[teardown] 关闭文件")
    f.close()


def test_write_file(open_file):
    open_file.write("hello fixture")
    assert not open_file.closed

执行流程:

  1. 执行 yield 前的 setup 代码;
  2. f 注入测试函数;
  3. 测试结束后,自动回溯执行 yield 后的 teardown 代码

✅ 官方推荐:优先使用 yield 方式实现 teardown,因其更简洁、不易遗漏(参见 [pytest 文档][1])。

此外,yield 的执行频率受 scope 控制

  • function:每次测试前后执行;
  • class:每个测试类前后执行;
  • module / session:对应粒度前后各执行一次。

九、usefixtures:只需执行,无需返回值

当你不需要 fixture 的返回值,但希望确保其副作用被执行 时,可使用 @pytest.mark.usefixtures

示例:

python 复制代码
# tests/test_usefixtures.py
import os
import pytest


@pytest.fixture
def prepare_env(monkeypatch):
    print("\n[fixture] 准备测试环境变量")
    monkeypatch.setenv("APP_ENV", "test")
    yield
    print("\n[teardown] 清理测试环境变量")


@pytest.mark.usefixtures("prepare_env")
class TestEnv:
    def test_env_value(self):
        assert os.getenv("APP_ENV") == "test"

    def test_other_case(self):
        assert os.getenv("APP_ENV") == "test"

特点:

  • prepare_env 在每个测试方法前自动执行;
  • 测试函数无法访问其返回值(本例中也无返回值);
  • 适用于纯副作用场景,如设置环境变量、启动服务等。

⚠️ 注意:不要将 @pytest.mark.usefixtures 用在 fixture 函数自身上,这会导致未定义行为,官方已明确不推荐(参见 [pytest 文档][1])。


十、参数化:params + ids,一套逻辑跑多组数据

示例代码

python 复制代码
# tests/test_param.py
import pytest


def fake_check_permission(username):
    return 200 if username == "admin" else 403


@pytest.fixture(
    params=[
        {"username": "admin", "expected": 200},
        {"username": "guest", "expected": 403},
    ],
    ids=["管理员场景", "访客场景"]
)
def case_data(request):
    return request.param


def test_permission(case_data):
    result = fake_check_permission(case_data["username"])
    assert result == case_data["expected"]

关键点

  • params:定义多组输入,fixture 会为每组参数执行一次;
  • request.param:获取当前轮次的参数;
  • ids:为每组测试生成可读性更强的标识名,便于调试和报告。

运行后,你会看到:

text 复制代码
test_permission[管理员场景]
test_permission[访客场景]

✅ 这是 pytest 实现数据驱动测试的标准方式(参见 [pytest 文档][1])。


十一、构建你的 fixture 思维模型

写 fixture 时,建议按以下顺序思考:

  1. 是否多处复用?

    → 是:提取为 fixture(如登录态、DB 连接、临时目录等)。

  2. 生命周期需要多长?

    • 单测独享 → function
    • 类内共享 → class
    • 模块共享 → module
    • 全局共享 → session
  3. 是否需要清理?

    • 否 → return
    • 是 → yield
  4. 测试是否需要返回值?

    • 需要 → 作为参数接收
    • 仅需副作用 → 用 usefixtures
  5. 是否要跑多组数据?

    • 是 → 使用 params + ids

这套思维,本质上是在利用 pytest fixture 的四大核心能力:

✅ 依赖注入|✅ 作用域复用|✅ yield 清理|✅ 参数化执行(参见 [pytest 文档][1])。


十二、综合流程图:从请求到清理

这张图完整串联了 fixture 的生命周期:

请求 → 查找 → 执行(setup)→ 注入 → 测试 → 执行(teardown)

相关推荐
赵谨言2 小时前
地球磁场干扰噪声减弱声波对抗测量系统研究进展:近十年中英文文献综述
大数据·开发语言·经验分享
jyan_敬言2 小时前
【算法】高精度算法(加减乘除)
c语言·开发语言·c++·笔记·算法
echome8882 小时前
Python 装饰器实战:用@syntax 优雅地增强函数功能
开发语言·python
路小雨~2 小时前
如何快速用测试用例来入门一个项目
python
不良人天码星2 小时前
GUI自动化基础(一)
python·ui·自动化
卷Java2 小时前
Python字典:键值对、get()方法、defaultdict,附通讯录实战
开发语言·数据库·python
liuyao_xianhui2 小时前
优选算法_翻转链表_头插法_C++
开发语言·数据结构·c++·算法·leetcode·链表·动态规划
happy_baymax2 小时前
三电平矢量表达式MATLAB实现
开发语言·matlab
xyq20242 小时前
jEasyUI 创建 XP 风格左侧面板
开发语言