【Python 测试开发】第6讲 | Fixture 与测试资源管理 - 优雅管理测试环境
环境声明
- Python 版本:Python 3.12+
- pytest 版本:pytest 8.3+
- 开发工具:PyCharm 或 VS Code
- 操作系统:Windows / macOS / Linux (通用)
学习目标
- 理解 Fixture 的核心概念与设计理念
- 掌握
@pytest.fixture装饰器的完整用法 - 灵活运用 Fixture 的五种作用域(function/class/module/session/package)
- 学会 Fixture 的依赖与嵌套调用
- 使用
conftest.py实现跨模块共享配置 - 掌握内置 Fixture 的使用场景
- 学会使用
yield和addfinalizer实现资源清理
一、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 连接池管理要点
- 会话级别:数据库连接在 session 作用域创建,避免重复连接开销
- 事务隔离:每个测试函数使用独立事务,测试后回滚
- 自动清理:使用 addfinalizer 确保会话结束时关闭连接
- 依赖注入:通过 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 的完整知识体系:
- Fixture 概念:理解 Fixture 作为测试资源管理的核心机制
- 基本用法:掌握
@pytest.fixture装饰器的参数和返回值 - 作用域管理:根据场景选择合适的 scope(function/class/module/package/session)
- 依赖注入:学会 Fixture 之间的依赖与嵌套调用
- 自动使用:了解 autouse 参数的使用场景
- 共享配置:使用 conftest.py 实现跨模块的 Fixture 共享
- 内置 Fixture:熟练使用 tmp_path、caplog、monkeypatch 等内置工具
- 资源清理:掌握 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 实现数据驱动
- 动态生成测试用例
- 结合参数化实现高效的边界值测试
敬请期待!
如果本文对你有帮助,欢迎点赞、收藏、评论。你的支持是我持续创作的动力!
更多推荐


所有评论(0)