环境声明

  • Python 版本:Python 3.12+
  • pytest 版本:pytest 8.3+
  • 开发工具:PyCharm 或 VS Code
  • 操作系统:Windows / macOS / Linux (通用)

学习目标

  1. 理解 Fixture 的核心概念与设计理念
  2. 掌握 @pytest.fixture 装饰器的完整用法
  3. 灵活运用 Fixture 的五种作用域(function/class/module/session/package)
  4. 学会 Fixture 的依赖与嵌套调用
  5. 使用 conftest.py 实现跨模块共享配置
  6. 掌握内置 Fixture 的使用场景
  7. 学会使用 yieldaddfinalizer 实现资源清理

一、Fixture 概念与优势

1.1 什么是 Fixture

Fixture 是 pytest 中用于管理测试资源的核心机制。它可以理解为"测试夹具",负责在测试执行前后准备和清理测试环境。

1.2 与传统 setup/teardown 的对比

特性 unittest setup/teardown pytest Fixture
作用域 固定(方法级/类级) 灵活(5种作用域可选)
复用性 难以跨类复用 通过 conftest.py 全局共享
依赖管理 手动管理 支持自动依赖注入
参数化 不支持 支持参数化 Fixture
代码组织 与测试类耦合 独立定义,按需使用

1.3 Fixture 的核心优势

  • 解耦:测试逻辑与资源准备分离
  • 复用:一次定义,多处使用
  • 灵活:按需注入,避免不必要的资源创建
  • 可组合:Fixture 可以相互依赖,形成资源链

二、@pytest.fixture 装饰器详解

2.1 基本用法

Fixture 使用 @pytest.fixture 装饰器定义,函数返回值即为 Fixture 提供的资源。

import pytest


@pytest.fixture
def sample_data():
    """返回测试用的示例数据"""
    return {"name": "Alice", "age": 25}


def test_user_info(sample_data):
    """使用 sample_data Fixture"""
    assert sample_data["name"] == "Alice"
    assert sample_data["age"] == 25

2.2 命名规范

  • Fixture 函数名应具有描述性,表明其提供的资源
  • 使用小写字母和下划线命名
  • 避免与测试函数重名
# 好的命名
@pytest.fixture
def database_connection():
    pass

@pytest.fixture
def api_client():
    pass

# 避免使用
@pytest.fixture
def fix1():  # 过于模糊
    pass

2.3 返回值类型

Fixture 可以返回任何类型的对象:

import pytest


@pytest.fixture
def simple_value():
    """返回基本类型"""
    return 42


@pytest.fixture
def list_data():
    """返回列表"""
    return [1, 2, 3, 4, 5]


@pytest.fixture
def dict_config():
    """返回字典配置"""
    return {
        "host": "localhost",
        "port": 3306,
        "database": "test_db"
    }


@pytest.fixture
def user_object():
    """返回自定义对象"""
    class User:
        def __init__(self, name):
            self.name = name
    
    return User("Bob")


def test_various_fixtures(simple_value, list_data, dict_config, user_object):
    """测试不同类型的 Fixture 返回值"""
    assert simple_value == 42
    assert len(list_data) == 5
    assert dict_config["host"] == "localhost"
    assert user_object.name == "Bob"

三、Fixture 作用域

Fixture 支持五种作用域,通过 scope 参数控制:

作用域 执行时机 适用场景
function(默认) 每个测试函数执行一次 需要独立的测试数据
class 每个测试类执行一次 类级别的共享资源
module 每个模块执行一次 模块级别的数据库连接
package 每个包执行一次 包级别的配置
session 整个测试会话执行一次 全局配置、连接池

3.1 function 作用域(默认)

import pytest


counter = 0


@pytest.fixture(scope="function")
def increment_counter():
    """每个测试函数都会重新执行"""
    global counter
    counter += 1
    return counter


def test_first(increment_counter):
    """counter = 1"""
    assert increment_counter == 1


def test_second(increment_counter):
    """counter = 1(重新初始化)"""
    assert increment_counter == 1

3.2 class 作用域

import pytest


counter = 0


@pytest.fixture(scope="class")
def shared_counter():
    """每个测试类只执行一次"""
    global counter
    counter += 1
    return counter


class TestGroupA:
    """测试类 A"""
    
    def test_a1(self, shared_counter):
        assert shared_counter == 1
    
    def test_a2(self, shared_counter):
        assert shared_counter == 1  # 同一个值


class TestGroupB:
    """测试类 B"""
    
    def test_b1(self, shared_counter):
        assert shared_counter == 2  # 新值

3.3 module 作用域

import pytest


@pytest.fixture(scope="module")
def module_resource():
    """整个模块共享的资源"""
    print("\n模块资源初始化")
    resource = {"data": "module_level", "calls": 0}
    yield resource
    print("\n模块资源清理")


def test_use_resource_1(module_resource):
    module_resource["calls"] += 1
    assert module_resource["calls"] == 1


def test_use_resource_2(module_resource):
    module_resource["calls"] += 1
    assert module_resource["calls"] == 2  # 共享同一个资源

3.4 session 作用域

import pytest


@pytest.fixture(scope="session")
def global_config():
    """整个测试会话共享的配置"""
    print("\n全局配置初始化")
    config = {
        "env": "testing",
        "debug": False,
        "timeout": 30
    }
    return config


# 这个 Fixture 可以在整个测试套件中使用
def test_with_global_config(global_config):
    assert global_config["env"] == "testing"

四、Fixture 依赖与嵌套

4.1 Fixture 调用 Fixture

Fixture 可以依赖其他 Fixture,形成资源链:

import pytest


@pytest.fixture
def database_url():
    """基础 Fixture:提供数据库连接字符串"""
    return "postgresql://localhost:5432/testdb"


@pytest.fixture
def database_connection(database_url):
    """依赖 database_url,建立连接"""
    print(f"\n连接到: {database_url}")
    connection = {"url": database_url, "connected": True}
    return connection


@pytest.fixture
def user_repository(database_connection):
    """依赖 database_connection,创建仓储层"""
    return {
        "connection": database_connection,
        "table": "users"
    }


def test_user_crud(user_repository):
    """使用嵌套依赖的 Fixture"""
    assert user_repository["connection"]["connected"] is True
    assert user_repository["table"] == "users"

4.2 多 Fixture 依赖

import pytest


@pytest.fixture
def api_key():
    return "secret_key_123"


@pytest.fixture
def api_endpoint():
    return "https://api.example.com"


@pytest.fixture
def api_client(api_key, api_endpoint):
    """依赖多个 Fixture"""
    return {
        "endpoint": api_endpoint,
        "headers": {"Authorization": f"Bearer {api_key}"}
    }


def test_api_call(api_client):
    assert api_client["endpoint"] == "https://api.example.com"
    assert "Bearer" in api_client["headers"]["Authorization"]

五、Fixture 自动使用

5.1 autouse 参数

设置 autouse=True 可以让 Fixture 自动应用于指定作用域内的所有测试,无需显式声明:

import pytest


@pytest.fixture(autouse=True, scope="function")
def setup_teardown():
    """自动在每个测试函数前后执行"""
    print("\n测试前准备")
    yield
    print("\n测试后清理")


def test_auto_1():
    """自动应用 setup_teardown"""
    assert True


def test_auto_2():
    """自动应用 setup_teardown"""
    assert True

5.2 类级别的 autouse

import pytest


class TestWithAutoSetup:
    
    @pytest.fixture(autouse=True, scope="class")
    def class_setup(self):
        """自动在类级别执行"""
        print("\n类初始化")
        self.shared_data = [1, 2, 3]
        yield
        print("\n类清理")
    
    def test_use_data_1(self):
        assert self.shared_data == [1, 2, 3]
    
    def test_use_data_2(self):
        self.shared_data.append(4)
        assert 4 in self.shared_data

六、conftest.py 共享配置

6.1 什么是 conftest.py

conftest.py 是 pytest 的特殊配置文件,用于定义共享的 Fixture 和钩子函数。

6.2 目录结构示例

project/
├── conftest.py          # 根目录:session 级共享
├── tests/
│   ├── conftest.py      # tests 目录:module 级共享
│   ├── unit/
│   │   ├── conftest.py  # unit 目录:class 级共享
│   │   └── test_unit.py
│   └── integration/
│       └── test_integration.py

6.3 conftest.py 内容示例

# conftest.py
import pytest


@pytest.fixture(scope="session")
def global_config():
    """所有测试共享的全局配置"""
    return {
        "database_url": "postgresql://localhost/test",
        "api_timeout": 30,
        "retry_count": 3
    }


@pytest.fixture(scope="module")
def module_logger():
    """模块级别的日志记录器"""
    import logging
    logger = logging.getLogger("test_module")
    return logger


@pytest.fixture
def temp_user():
    """创建临时用户"""
    return {"id": 1, "username": "test_user", "email": "test@example.com"}

6.4 层级作用域

子目录的 conftest.py 会继承父目录的配置,也可以覆盖:

# tests/conftest.py
import pytest


@pytest.fixture
def database():
    """父级配置:通用数据库"""
    return {"type": "generic", "name": "test_db"}
# tests/integration/conftest.py
import pytest


@pytest.fixture
def database():
    """子级配置:覆盖为集成测试专用数据库"""
    return {"type": "integration", "name": "integration_db"}

七、内置 Fixture 介绍

pytest 提供了丰富的内置 Fixture,开箱即用:

7.1 tmpdir 与 tmp_path

用于创建临时文件和目录:

def test_tmpdir(tmpdir):
    """使用 tmpdir(返回 py.path.local 对象)"""
    # 创建临时文件
    temp_file = tmpdir.join("test.txt")
    temp_file.write("Hello, World!")
    
    assert temp_file.read() == "Hello, World!"


def test_tmp_path(tmp_path):
    """使用 tmp_path(返回 pathlib.Path 对象,推荐)"""
    # 创建临时目录
    temp_dir = tmp_path / "subdir"
    temp_dir.mkdir()
    
    # 创建临时文件
    temp_file = temp_dir / "data.json"
    temp_file.write_text('{"key": "value"}')
    
    assert temp_file.exists()
    assert temp_file.read_text() == '{"key": "value"}'

7.2 caplog 日志捕获

import logging


def test_logging(caplog):
    """捕获日志输出"""
    # 设置日志级别
    caplog.set_level(logging.INFO)
    
    # 记录日志
    logging.info("这是一条信息日志")
    logging.warning("这是一条警告日志")
    
    # 验证日志内容
    assert "信息日志" in caplog.text
    assert "警告日志" in caplog.text
    
    # 检查特定级别的日志
    assert len(caplog.records) == 2
    assert caplog.records[0].levelname == "INFO"

7.3 capfd 捕获文件描述符

def test_stdout_capture(capfd):
    """捕获标准输出"""
    print("Hello, stdout!")
    print("Error message", file=__import__('sys').stderr)
    
    # 捕获输出
    captured = capfd.readouterr()
    
    assert "Hello, stdout!" in captured.out
    assert "Error message" in captured.err

7.4 monkeypatch 猴子补丁

def test_monkeypatch_env(monkeypatch):
    """修改环境变量"""
    monkeypatch.setenv("API_KEY", "test_key_123")
    
    import os
    assert os.environ["API_KEY"] == "test_key_123"


def test_monkeypatch_attr(monkeypatch):
    """修改对象属性"""
    class Config:
        timeout = 30
    
    # 修改属性
    monkeypatch.setattr(Config, "timeout", 60)
    
    assert Config.timeout == 60


def test_monkeypatch_dict(monkeypatch):
    """修改字典"""
    settings = {"debug": False, "port": 8080}
    
    monkeypatch.setitem(settings, "debug", True)
    
    assert settings["debug"] is True

八、yield 与清理逻辑

8.1 yield 实现 setup/teardown

使用 yield 可以将 Fixture 分为两部分:yield 之前是 setup,之后是 teardown。

import pytest


@pytest.fixture
def managed_resource():
    """使用 yield 实现资源管理"""
    # Setup 阶段
    print("\n初始化资源")
    resource = {"status": "ready", "data": []}
    
    yield resource  # 返回资源给测试函数
    
    # Teardown 阶段(yield 之后)
    print("\n清理资源")
    resource["status"] = "closed"
    resource["data"].clear()


def test_with_resource(managed_resource):
    """使用受管理的资源"""
    managed_resource["data"].append("test_data")
    assert managed_resource["status"] == "ready"
    assert len(managed_resource["data"]) == 1

8.2 异常处理

yield 之后的清理代码即使测试失败也会执行:

import pytest


@pytest.fixture
def robust_resource():
    """即使测试失败也会执行清理"""
    resource = {"opened": True}
    print("\n资源打开")
    
    try:
        yield resource
    finally:
        # 无论测试结果如何,都会执行
        resource["opened"] = False
        print("\n资源关闭")


def test_that_fails(robust_resource):
    """这个测试会失败,但清理仍会执行"""
    assert robust_resource["opened"] is True
    # 断言失败,但上面的 finally 块仍会执行
    # assert False  # 取消注释以测试失败场景

九、addfinalizer 替代方案

9.1 使用 request.addfinalizer

除了 yield,还可以使用 request.addfinalizer 注册清理函数:

import pytest


@pytest.fixture
def resource_with_finalizer(request):
    """使用 addfinalizer 注册清理"""
    resource = {"name": "test_resource", "active": True}
    print("\n资源创建")
    
    def cleanup():
        """清理函数"""
        resource["active"] = False
        print("\n资源通过 finalizer 清理")
    
    # 注册清理函数
    request.addfinalizer(cleanup)
    
    return resource


def test_with_finalizer(resource_with_finalizer):
    assert resource_with_finalizer["active"] is True

9.2 多个 finalizer

可以注册多个清理函数,它们会按注册顺序的逆序执行:

import pytest


@pytest.fixture
def multi_cleanup_resource(request):
    """注册多个清理函数"""
    resources = []
    
    def cleanup_a():
        print("\n清理 A")
        if "A" in resources:
            resources.remove("A")
    
    def cleanup_b():
        print("\n清理 B")
        if "B" in resources:
            resources.remove("B")
    
    request.addfinalizer(cleanup_a)
    request.addfinalizer(cleanup_b)
    
    resources.extend(["A", "B"])
    return resources


def test_multi_cleanup(multi_cleanup_resource):
    assert len(multi_cleanup_resource) == 2

9.3 yield vs addfinalizer 选择指南

场景 推荐方式
简单的资源管理 yield
条件性清理 addfinalizer
多个独立清理步骤 addfinalizer
需要在清理时访问异常信息 addfinalizer

十、实战案例:数据库连接 Fixture

10.1 完整的数据库连接管理

# conftest.py
import pytest
import sqlite3


class DatabaseConnection:
    """数据库连接包装类"""
    
    def __init__(self, db_path):
        self.db_path = db_path
        self.connection = None
    
    def connect(self):
        self.connection = sqlite3.connect(self.db_path)
        return self.connection
    
    def close(self):
        if self.connection:
            self.connection.close()
            self.connection = None
    
    def execute(self, query, params=None):
        cursor = self.connection.cursor()
        if params:
            cursor.execute(query, params)
        else:
            cursor.execute(query)
        self.connection.commit()
        return cursor


@pytest.fixture(scope="session")
def db_connection_pool(request):
    """会话级别的数据库连接池"""
    print("\n初始化数据库连接池")
    
    # 创建内存数据库连接
    db = DatabaseConnection(":memory:")
    conn = db.connect()
    
    # 初始化表结构
    conn.execute("""
        CREATE TABLE users (
            id INTEGER PRIMARY KEY,
            username TEXT NOT NULL,
            email TEXT
        )
    """)
    
    def cleanup():
        """会话结束时清理"""
        print("\n关闭数据库连接池")
        db.close()
    
    request.addfinalizer(cleanup)
    
    return db


@pytest.fixture(scope="function")
def db_transaction(db_connection_pool):
    """函数级别的事务管理"""
    conn = db_connection_pool.connection
    
    # 开始事务
    conn.execute("BEGIN")
    
    yield db_connection_pool
    
    # 回滚事务(保持测试隔离性)
    conn.execute("ROLLBACK")


@pytest.fixture
def test_user(db_transaction):
    """创建测试用户"""
    db_transaction.execute(
        "INSERT INTO users (username, email) VALUES (?, ?)",
        ("testuser", "test@example.com")
    )
    
    cursor = db_transaction.execute("SELECT * FROM users WHERE username = ?", ("testuser",))
    user = cursor.fetchone()
    
    return {
        "id": user[0],
        "username": user[1],
        "email": user[2]
    }

10.2 测试用例

# test_database.py


def test_create_user(db_transaction, test_user):
    """测试用户创建"""
    assert test_user["username"] == "testuser"
    assert test_user["email"] == "test@example.com"


def test_user_isolation(db_transaction):
    """测试事务隔离性 - 上一个测试的数据不应存在"""
    cursor = db_transaction.execute("SELECT COUNT(*) FROM users")
    count = cursor.fetchone()[0]
    assert count == 0  # 由于回滚,数据已被清理


def test_insert_and_query(db_transaction):
    """测试插入和查询"""
    # 插入数据
    db_transaction.execute(
        "INSERT INTO users (username, email) VALUES (?, ?)",
        ("alice", "alice@example.com")
    )
    
    # 查询数据
    cursor = db_transaction.execute("SELECT * FROM users WHERE username = ?", ("alice",))
    user = cursor.fetchone()
    
    assert user[1] == "alice"
    assert user[2] == "alice@example.com"

10.3 连接池管理要点

  1. 会话级别:数据库连接在 session 作用域创建,避免重复连接开销
  2. 事务隔离:每个测试函数使用独立事务,测试后回滚
  3. 自动清理:使用 addfinalizer 确保会话结束时关闭连接
  4. 依赖注入:通过 Fixture 链实现资源的层级管理

十一、避坑小贴士

11.1 作用域不匹配

# 错误示例
@pytest.fixture(scope="module")
def module_data():
    return {"count": 0}


@pytest.fixture(scope="function")
def bad_dependency(module_data):
    """错误:function 作用域依赖 module 作用域"""
    # 虽然可以运行,但逻辑上容易混淆
    module_data["count"] += 1
    return module_data

11.2 修改可变对象

# 问题示例
@pytest.fixture
def shared_list():
    return []  # 可变对象


def test_modify_list_1(shared_list):
    shared_list.append(1)
    assert shared_list == [1]


def test_modify_list_2(shared_list):
    # 可能受到上一个测试的影响!
    # 如果作用域不是 function,这里可能看到 [1]
    pass

解决方案:使用 factory 模式

@pytest.fixture
def list_factory():
    """返回工厂函数,每次调用创建新列表"""
    def _create_list():
        return []
    return _create_list


def test_with_factory(list_factory):
    my_list = list_factory()
    my_list.append(1)
    assert my_list == [1]

11.3 Fixture 循环依赖

# 错误:循环依赖
@pytest.fixture
def fixture_a(fixture_b):
    return "a"


@pytest.fixture
def fixture_b(fixture_a):
    return "b"  # 会导致无限递归

11.4 autouse 的滥用

# 不推荐:全局 autouse 难以追踪
@pytest.fixture(autouse=True, scope="session")
def global_setup():
    # 所有测试都会受到影响,调试困难
    pass

建议:显式声明依赖,代码更易理解


十二、本章小结

本章深入学习了 pytest Fixture 的完整知识体系:

  1. Fixture 概念:理解 Fixture 作为测试资源管理的核心机制
  2. 基本用法:掌握 @pytest.fixture 装饰器的参数和返回值
  3. 作用域管理:根据场景选择合适的 scope(function/class/module/package/session)
  4. 依赖注入:学会 Fixture 之间的依赖与嵌套调用
  5. 自动使用:了解 autouse 参数的使用场景
  6. 共享配置:使用 conftest.py 实现跨模块的 Fixture 共享
  7. 内置 Fixture:熟练使用 tmp_path、caplog、monkeypatch 等内置工具
  8. 资源清理:掌握 yield 和 addfinalizer 两种清理方式

一句话总结:Fixture 是 pytest 最强大的特性之一,它让测试资源管理变得优雅、灵活且可复用。


十三、课后练习

练习 1:基础 Fixture

编写一个 Fixture,生成随机的测试用户数据(包含用户名、邮箱、年龄),并在 3 个测试函数中使用。

练习 2:作用域对比

创建两个 Fixture,一个 scope=“function”,一个 scope=“module”,分别记录被调用的次数,观察不同作用域的行为差异。

练习 3:Fixture 依赖链

实现以下依赖链:

  • config Fixture 提供配置
  • database Fixture 依赖 config 创建连接
  • user_repository Fixture 依赖 database 提供用户操作
  • 编写测试使用 user_repository

练习 4:资源清理

使用 yield 实现一个文件操作的 Fixture:

  • Setup:创建临时文件并写入初始数据
  • Teardown:删除临时文件
  • 验证:测试文件在测试中可读写,测试后已清理

练习 5:conftest.py 实践

在一个测试目录中创建 conftest.py,定义:

  • 一个 session 级别的配置 Fixture
  • 一个 module 级别的日志 Fixture
  • 在多个测试模块中使用这些共享 Fixture

十四、下一篇预告

【Python 测试开发】第7讲 | 参数化测试与数据驱动 - 用数据说话

在下一讲中,我们将学习:

  • @pytest.mark.parametrize 的多种用法
  • 从文件/数据库加载测试数据
  • 参数化 Fixture 实现数据驱动
  • 动态生成测试用例
  • 结合参数化实现高效的边界值测试

敬请期待!


如果本文对你有帮助,欢迎点赞、收藏、评论。你的支持是我持续创作的动力!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐