Pytest 中的 @pytest.mark.usefixtures 详解
1. 什么是 usefixtures
1.1 基本概念
@pytest.mark.usefixtures 是 pytest 提供的一个装饰器,用于在测试类或测试函数上使用 fixture,而不需要将 fixture 作为参数传递。
1.2 形象比喻
想象一下,你要使用一个工具:
普通 fixture 使用方式(需要参数):
- 就像你每次要用锤子时,都要说”给我锤子”(在函数参数中声明)
- 然后你才能使用这个锤子
usefixtures 使用方式(不需要参数):
- 就像你进入一个”工具房”(使用装饰器标记)
- 工具房里已经准备好了所有工具(fixture 自动执行)
- 你不需要明确说”给我锤子”,工具已经在那里了
- 但你不能直接拿到工具本身(不能访问 fixture 的返回值)
1.3 usefixtures 的作用
usefixtures 主要有以下几个作用:
- 自动执行 fixture:不需要在测试函数参数中声明,fixture 会自动执行
- 简化测试函数签名:测试函数不需要添加 fixture 参数,保持函数签名简洁
- 批量应用 fixture:可以给整个测试类应用 fixture,类中所有测试都会自动使用
- 只执行副作用:适合那些只需要执行前置/后置操作,不需要返回值的 fixture
1.4 为什么需要 usefixtures
在实际测试中,你经常会遇到以下场景:
场景 1:只需要执行操作,不需要返回值
# 普通 fixture 使用方式
@pytest.fixture
def setup_database():
"""初始化数据库"""
print("连接数据库...")
# 执行一些初始化操作,但不需要返回任何值
create_test_tables()
yield
print("清理数据库...")
drop_test_tables()
def test_query_user(setup_database): # 必须声明参数,即使不需要使用返回值
"""测试查询用户"""
# 实际上我们不需要 setup_database 的值,只需要它执行初始化
result = query_user("张三")
assert result is not None
问题:
- 测试函数必须声明
setup_database参数,即使不需要使用它 - 函数签名变得冗长,特别是当有多个这样的 fixture 时
- 容易让人误解,以为需要用到
setup_database的返回值
使用 usefixtures 后:
@pytest.fixture
def setup_database():
"""初始化数据库"""
print("连接数据库...")
create_test_tables()
yield
print("清理数据库...")
drop_test_tables()
@pytest.mark.usefixtures("setup_database")
def test_query_user(): # 不需要声明参数,函数签名更简洁
"""测试查询用户"""
# fixture 会自动执行,但我们不需要访问它的返回值
result = query_user("张三")
assert result is not None
优势:
- 测试函数签名更简洁,不需要声明不需要的参数
- 代码更清晰,明确表示只需要执行 fixture,不需要返回值
- 适合那些只有”副作用”(side effects)的 fixture
场景 2:给整个测试类应用 fixture
# 普通方式:每个测试函数都要声明参数
class TestUserAPI:
def test_create_user(self, setup_database, setup_cache): # 每个都要写
pass
def test_delete_user(self, setup_database, setup_cache): # 重复声明
pass
def test_update_user(self, setup_database, setup_cache): # 重复声明
pass
使用 usefixtures 后:
@pytest.mark.usefixtures("setup_database", "setup_cache")
class TestUserAPI:
def test_create_user(self): # 不需要声明参数
pass
def test_delete_user(self): # 不需要声明参数
pass
def test_update_user(self): # 不需要声明参数
pass
优势:
- 整个类的所有测试都会自动使用这些 fixture
- 不需要在每个测试函数中重复声明
- 代码更简洁,维护更方便
2. usefixtures 的基本语法
2.1 基本语法格式
@pytest.mark.usefixtures("fixture_name1", "fixture_name2", ...)
def test_function():
pass
或者用于测试类:
@pytest.mark.usefixtures("fixture_name1", "fixture_name2", ...)
class TestClass:
def test_method(self):
pass
2.2 最简单的示例
2.2.1 单个 fixture
import pytest
@pytest.fixture
def setup_data():
"""准备测试数据"""
print("准备测试数据...")
yield
print("清理测试数据...")
@pytest.mark.usefixtures("setup_data")
def test_example():
"""使用 usefixtures 的测试"""
print("执行测试...")
assert True
运行结果:
准备测试数据...
执行测试...
清理测试数据...
2.2.2 多个 fixture
import pytest
@pytest.fixture
def setup_database():
print("初始化数据库...")
yield
print("清理数据库...")
@pytest.fixture
def setup_cache():
print("初始化缓存...")
yield
print("清理缓存...")
@pytest.mark.usefixtures("setup_database", "setup_cache")
def test_complex():
"""使用多个 fixture"""
print("执行复杂测试...")
assert True
运行结果:
初始化数据库...
初始化缓存...
执行复杂测试...
清理缓存...
清理数据库...
注意:fixture 的执行顺序是按照在 usefixtures 中声明的顺序,清理顺序相反(后进先出)。
3. usefixtures 与普通 fixture 使用的区别
3.1 核心区别对比
| 特性 | 普通 fixture 使用 | usefixtures |
|---|---|---|
| 参数声明 | 必须在测试函数参数中声明 | 不需要在参数中声明 |
| 访问返回值 | 可以访问 fixture 的返回值 | 不能访问 fixture 的返回值 |
| 使用场景 | 需要 fixture 返回的数据 | 只需要执行 fixture 的副作用 |
| 函数签名 | 参数较多时签名较长 | 函数签名简洁 |
| 适用对象 | 测试函数、测试类 | 测试函数、测试类 |
3.2 详细对比示例
3.2.1 需要返回值的场景(必须用普通方式)
import pytest
@pytest.fixture
def user_data():
"""返回用户数据"""
return {"name": "张三", "age": 25}
# ✅ 正确:需要访问返回值,必须用普通方式
def test_user_info(user_data):
"""需要访问 fixture 的返回值"""
assert user_data["name"] == "张三"
assert user_data["age"] == 25
# ❌ 错误:usefixtures 无法访问返回值
@pytest.mark.usefixtures("user_data")
def test_user_info_wrong():
"""这样写是错误的,无法访问 user_data"""
# 这里无法访问 user_data,会报错
# assert user_data["name"] == "张三" # NameError: name 'user_data' is not defined
pass
3.2.2 只需要副作用的场景(两种方式都可以)
import pytest
@pytest.fixture
def setup_environment():
"""设置测试环境(只有副作用,无返回值)"""
print("设置测试环境...")
import os
os.environ["TEST_MODE"] = "true"
yield
print("清理测试环境...")
os.environ.pop("TEST_MODE", None)
# 方式 1:普通 fixture 使用(可以,但不优雅)
def test_with_parameter(setup_environment):
"""使用参数方式,但不需要 setup_environment 的值"""
# setup_environment 的值是 None(因为 fixture 没有 return)
# 我们实际上不需要这个值,只是需要它执行
assert os.environ.get("TEST_MODE") == "true"
# 方式 2:usefixtures(更优雅,推荐)
@pytest.mark.usefixtures("setup_environment")
def test_with_usefixtures():
"""使用 usefixtures,函数签名更简洁"""
assert os.environ.get("TEST_MODE") == "true"
3.2.3 混合使用场景
import pytest
@pytest.fixture
def setup_database():
"""初始化数据库(只需要副作用)"""
print("连接数据库...")
yield
print("关闭数据库...")
@pytest.fixture
def user_data():
"""返回用户数据(需要返回值)"""
return {"name": "张三", "age": 25}
# 混合使用:usefixtures + 普通参数
@pytest.mark.usefixtures("setup_database")
def test_user_operation(user_data):
"""setup_database 用 usefixtures,user_data 用普通方式"""
# setup_database 自动执行,不需要声明
# user_data 需要返回值,所以用普通方式
assert user_data["name"] == "张三"
print("执行用户操作测试...")
4. usefixtures 在测试类中的应用
4.1 给整个类应用 fixture
4.1.1 基本用法
import pytest
@pytest.fixture
def setup_database():
"""初始化数据库"""
print("=== 连接数据库 ===")
yield
print("=== 关闭数据库 ===")
@pytest.fixture
def setup_cache():
"""初始化缓存"""
print("=== 初始化缓存 ===")
yield
print("=== 清理缓存 ===")
@pytest.mark.usefixtures("setup_database", "setup_cache")
class TestUserAPI:
"""整个类都会自动使用 setup_database 和 setup_cache"""
def test_create_user(self):
"""创建用户测试"""
print("执行创建用户测试...")
assert True
def test_delete_user(self):
"""删除用户测试"""
print("执行删除用户测试...")
assert True
def test_update_user(self):
"""更新用户测试"""
print("执行更新用户测试...")
assert True
运行结果:
=== 连接数据库 ===
=== 初始化缓存 ===
执行创建用户测试...
=== 清理缓存 ===
=== 关闭数据库 ===
=== 连接数据库 ===
=== 初始化缓存 ===
执行删除用户测试...
=== 清理缓存 ===
=== 关闭数据库 ===
=== 连接数据库 ===
=== 初始化缓存 ===
执行更新用户测试...
=== 清理缓存 ===
=== 关闭数据库 ===
注意:默认情况下,每个测试方法都会执行一次 fixture(scope=”function”)。
4.1.2 使用 class scope 的 fixture
import pytest
@pytest.fixture(scope="class")
def setup_database():
"""类级别的数据库初始化(整个类只执行一次)"""
print("=== 连接数据库(类级别)===")
yield
print("=== 关闭数据库(类级别)===")
@pytest.fixture(scope="class")
def setup_cache():
"""类级别的缓存初始化(整个类只执行一次)"""
print("=== 初始化缓存(类级别)===")
yield
print("=== 清理缓存(类级别)===")
@pytest.mark.usefixtures("setup_database", "setup_cache")
class TestUserAPI:
"""整个类共享同一个数据库连接和缓存"""
def test_create_user(self):
"""创建用户测试"""
print("执行创建用户测试...")
assert True
def test_delete_user(self):
"""删除用户测试"""
print("执行删除用户测试...")
assert True
def test_update_user(self):
"""更新用户测试"""
print("执行更新用户测试...")
assert True
运行结果:
=== 连接数据库(类级别)===
=== 初始化缓存(类级别)===
执行创建用户测试...
执行删除用户测试...
执行更新用户测试...
=== 清理缓存(类级别)===
=== 关闭数据库(类级别)===
优势:整个类的所有测试共享同一个数据库连接和缓存,性能更好。
4.2 类级别和函数级别的混合使用
import pytest
@pytest.fixture(scope="class")
def setup_database():
"""类级别的数据库连接"""
print("=== 连接数据库(类级别)===")
yield
print("=== 关闭数据库(类级别)===")
@pytest.fixture
def clean_data():
"""函数级别的数据清理(每个测试都执行)"""
print("--- 清理测试数据(函数级别)---")
yield
print("--- 完成数据清理(函数级别)---")
@pytest.mark.usefixtures("setup_database")
class TestUserAPI:
"""类级别使用 setup_database"""
@pytest.mark.usefixtures("clean_data")
def test_create_user(self):
"""函数级别额外使用 clean_data"""
print("执行创建用户测试...")
assert True
def test_delete_user(self):
"""只使用类级别的 setup_database"""
print("执行删除用户测试...")
assert True
运行结果:
=== 连接数据库(类级别)===
--- 清理测试数据(函数级别)---
执行创建用户测试...
--- 完成数据清理(函数级别)---
--- 清理测试数据(函数级别)---
执行删除用户测试...
--- 完成数据清理(函数级别)---
=== 关闭数据库(类级别)===
4.3 继承中的 usefixtures
import pytest
@pytest.fixture
def base_setup():
"""基础设置"""
print("=== 基础设置 ===")
yield
print("=== 基础清理 ===")
@pytest.mark.usefixtures("base_setup")
class BaseTest:
"""基类,定义了基础 fixture"""
pass
@pytest.fixture
def api_setup():
"""API 特定设置"""
print("=== API 设置 ===")
yield
print("=== API 清理 ===")
@pytest.mark.usefixtures("api_setup")
class TestUserAPI(BaseTest):
"""继承基类,同时添加自己的 fixture"""
def test_create_user(self):
"""会同时执行 base_setup 和 api_setup"""
print("执行创建用户测试...")
assert True
运行结果:
=== 基础设置 ===
=== API 设置 ===
执行创建用户测试...
=== API 清理 ===
=== 基础清理 ===
5. usefixtures 的常见使用场景
5.1 场景 1:数据库初始化和清理
import pytest
@pytest.fixture
def setup_database():
"""数据库初始化和清理"""
print("连接测试数据库...")
# 创建测试表
create_test_tables()
yield
print("清理测试数据...")
# 删除测试表
drop_test_tables()
@pytest.mark.usefixtures("setup_database")
def test_insert_user():
"""测试插入用户"""
# 不需要访问数据库连接对象,只需要确保数据库已初始化
result = insert_user("张三", "zhangsan@example.com")
assert result is not None
@pytest.mark.usefixtures("setup_database")
def test_query_user():
"""测试查询用户"""
# 不需要访问数据库连接对象
user = query_user("张三")
assert user is not None
5.2 场景 2:环境变量设置
import pytest
import os
@pytest.fixture
def set_test_env():
"""设置测试环境变量"""
original_env = os.environ.copy()
os.environ["TEST_MODE"] = "true"
os.environ["DEBUG"] = "false"
yield
# 恢复原始环境变量
os.environ.clear()
os.environ.update(original_env)
@pytest.mark.usefixtures("set_test_env")
def test_with_test_mode():
"""在测试模式下运行的测试"""
assert os.environ.get("TEST_MODE") == "true"
assert os.environ.get("DEBUG") == "false"
@pytest.mark.usefixtures("set_test_env")
def test_another_with_test_mode():
"""另一个需要测试模式的测试"""
# 环境变量已经设置好了
assert "TEST_MODE" in os.environ
5.3 场景 3:日志配置
import pytest
import logging
@pytest.fixture
def setup_logging():
"""配置测试日志"""
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
yield
# 清理日志配置
logging.shutdown()
@pytest.mark.usefixtures("setup_logging")
def test_with_logging():
"""使用日志的测试"""
logger = logging.getLogger(__name__)
logger.debug("这是一条调试日志")
logger.info("这是一条信息日志")
assert True
5.4 场景 4:临时文件创建和清理
import pytest
import tempfile
import os
@pytest.fixture
def temp_directory():
"""创建临时目录"""
temp_dir = tempfile.mkdtemp()
print(f"创建临时目录:{temp_dir}")
yield temp_dir
# 清理临时目录
import shutil
shutil.rmtree(temp_dir)
print(f"删除临时目录:{temp_dir}")
# 注意:如果需要访问 temp_directory 的路径,不能用 usefixtures
def test_with_temp_dir(temp_directory):
"""需要访问临时目录路径,用普通方式"""
test_file = os.path.join(temp_directory, "test.txt")
with open(test_file, "w") as f:
f.write("test content")
assert os.path.exists(test_file)
@pytest.fixture
def setup_temp_files():
"""只创建临时文件,不需要返回路径"""
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".txt")
temp_file.write(b"test content")
temp_file.close()
print(f"创建临时文件:{temp_file.name}")
yield
# 清理临时文件
os.unlink(temp_file.name)
print(f"删除临时文件:{temp_file.name}")
@pytest.mark.usefixtures("setup_temp_files")
def test_with_temp_files():
"""只需要临时文件存在,不需要访问路径"""
# 测试逻辑,临时文件已经创建好了
assert True
5.5 场景 5:Mock 对象设置
import pytest
from unittest.mock import patch
@pytest.fixture
def mock_external_api():
"""Mock 外部 API"""
with patch('requests.get') as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"status": "ok"}
yield
@pytest.mark.usefixtures("mock_external_api")
def test_api_call():
"""测试 API 调用(外部 API 已被 Mock)"""
import requests
response = requests.get("http://example.com/api")
assert response.status_code == 200
assert response.json()["status"] == "ok"
5.6 场景 6:Web 驱动初始化(Selenium)
import pytest
from selenium import webdriver
@pytest.fixture
def setup_browser():
"""初始化浏览器"""
driver = webdriver.Chrome()
driver.maximize_window()
yield driver
driver.quit()
# 注意:如果需要操作浏览器,必须用普通方式获取 driver 对象
def test_login(setup_browser):
"""需要操作浏览器,用普通方式"""
driver = setup_browser
driver.get("http://example.com")
assert "Example" in driver.title
@pytest.fixture
def setup_test_environment():
"""只设置测试环境,不需要返回 driver"""
# 设置一些环境变量或配置
import os
os.environ["BROWSER"] = "chrome"
yield
os.environ.pop("BROWSER", None)
@pytest.mark.usefixtures("setup_test_environment")
def test_environment_check():
"""只需要环境设置,不需要操作浏览器"""
import os
assert os.environ.get("BROWSER") == "chrome"
5.7 场景 7:多个 fixture 的组合使用
import pytest
@pytest.fixture
def setup_database():
"""数据库设置"""
print("1. 初始化数据库")
yield
print("1. 清理数据库")
@pytest.fixture
def setup_cache():
"""缓存设置"""
print("2. 初始化缓存")
yield
print("2. 清理缓存")
@pytest.fixture
def setup_logging():
"""日志设置"""
print("3. 配置日志")
yield
print("3. 关闭日志")
@pytest.fixture
def user_data():
"""用户数据(需要返回值)"""
return {"name": "张三", "age": 25}
@pytest.mark.usefixtures("setup_database", "setup_cache", "setup_logging")
class TestUserOperations:
"""使用多个 usefixtures + 一个普通 fixture"""
def test_create_user(self, user_data):
"""创建用户"""
# setup_database, setup_cache, setup_logging 自动执行
# user_data 需要返回值,所以用普通方式
print(f"创建用户:{user_data['name']}")
assert user_data["name"] == "张三"
def test_update_user(self, user_data):
"""更新用户"""
print(f"更新用户:{user_data['name']}")
assert user_data["age"] == 25
运行结果:
1. 初始化数据库
2. 初始化缓存
3. 配置日志
创建用户:张三
3. 关闭日志
2. 清理缓存
1. 清理数据库
1. 初始化数据库
2. 初始化缓存
3. 配置日志
更新用户:张三
3. 关闭日志
2. 清理缓存
1. 清理数据库
6. usefixtures 的执行顺序
6.1 基本执行顺序规则
- fixture 的执行顺序:按照在
usefixtures中声明的顺序执行 - 清理顺序:与执行顺序相反(后进先出,LIFO)
- 与普通 fixture 参数的顺序:usefixtures 的 fixture 先执行,然后是普通参数 fixture
6.2 详细示例
import pytest
@pytest.fixture
def fixture_a():
print("执行 fixture_a")
yield
print("清理 fixture_a")
@pytest.fixture
def fixture_b():
print("执行 fixture_b")
yield
print("清理 fixture_b")
@pytest.fixture
def fixture_c():
print("执行 fixture_c")
yield
print("清理 fixture_c")
@pytest.fixture
def data_fixture():
print("执行 data_fixture")
return "data"
# 注意:没有 yield,所以没有清理步骤
@pytest.mark.usefixtures("fixture_a", "fixture_b", "fixture_c")
def test_order(data_fixture):
"""测试执行顺序"""
print("执行测试函数")
assert data_fixture == "data"
运行结果:
执行 fixture_a
执行 fixture_b
执行 fixture_c
执行 data_fixture
执行测试函数
清理 fixture_c
清理 fixture_b
清理 fixture_a
执行顺序说明:
- 先执行
usefixtures中的 fixture(按声明顺序):fixture_a→fixture_b→fixture_c - 然后执行普通参数 fixture:
data_fixture - 最后执行测试函数
- 清理顺序相反:
fixture_c→fixture_b→fixture_a
6.3 类级别和函数级别的执行顺序
import pytest
@pytest.fixture(scope="class")
def class_fixture():
print("执行 class_fixture(类级别)")
yield
print("清理 class_fixture(类级别)")
@pytest.fixture
def function_fixture():
print("执行 function_fixture(函数级别)")
yield
print("清理 function_fixture(函数级别)")
@pytest.mark.usefixtures("class_fixture")
class TestOrder:
"""类级别使用 class_fixture"""
@pytest.mark.usefixtures("function_fixture")
def test_method(self):
"""函数级别使用 function_fixture"""
print("执行测试方法")
assert True
运行结果:
执行 class_fixture(类级别)
执行 function_fixture(函数级别)
执行测试方法
清理 function_fixture(函数级别)
清理 class_fixture(类级别)
执行顺序说明:
- 先执行类级别的 fixture:
class_fixture - 然后执行函数级别的 fixture:
function_fixture - 执行测试方法
- 清理顺序相反:先清理函数级别,再清理类级别
7. usefixtures 的注意事项和常见错误
7.1 注意事项
7.1.1 不能访问 fixture 的返回值
import pytest
@pytest.fixture
def user_data():
"""返回用户数据"""
return {"name": "张三", "age": 25}
# ❌ 错误:usefixtures 无法访问返回值
@pytest.mark.usefixtures("user_data")
def test_wrong():
# 这里无法访问 user_data
# print(user_data) # NameError: name 'user_data' is not defined
pass
# ✅ 正确:需要返回值时用普通方式
def test_correct(user_data):
print(user_data) # 可以正常访问
assert user_data["name"] == "张三"
7.1.2 fixture 名称必须是字符串
import pytest
@pytest.fixture
def my_fixture():
return "data"
# ✅ 正确:使用字符串
@pytest.mark.usefixtures("my_fixture")
def test_correct():
pass
# ❌ 错误:不能直接使用变量
# @pytest.mark.usefixtures(my_fixture) # 这样会报错
7.1.3 fixture 必须存在
import pytest
# ❌ 错误:fixture 不存在
@pytest.mark.usefixtures("non_existent_fixture")
def test_wrong():
pass
运行时会报错:
E Fixture "non_existent_fixture" not found
7.1.4 作用域的影响
import pytest
@pytest.fixture(scope="session")
def session_fixture():
print("执行 session_fixture(整个会话一次)")
yield
print("清理 session_fixture")
@pytest.fixture(scope="module")
def module_fixture():
print("执行 module_fixture(每个模块一次)")
yield
print("清理 module_fixture")
@pytest.fixture(scope="class")
def class_fixture():
print("执行 class_fixture(每个类一次)")
yield
print("清理 class_fixture")
@pytest.fixture # 默认 scope="function"
def function_fixture():
print("执行 function_fixture(每个函数一次)")
yield
print("清理 function_fixture")
@pytest.mark.usefixtures("session_fixture", "module_fixture", "class_fixture", "function_fixture")
class TestScope:
def test_1(self):
pass
def test_2(self):
pass
运行结果:
执行 session_fixture(整个会话一次)
执行 module_fixture(每个模块一次)
执行 class_fixture(每个类一次)
执行 function_fixture(每个函数一次)
执行测试 test_1
清理 function_fixture(每个函数一次)
执行 function_fixture(每个函数一次)
执行测试 test_2
清理 function_fixture(每个函数一次)
清理 class_fixture(每个类一次)
清理 module_fixture(每个模块一次)
清理 session_fixture(整个会话一次)
7.2 常见错误和解决方案
错误 1:试图访问 usefixtures 的返回值
错误代码:
import pytest
@pytest.fixture
def get_data():
return {"key": "value"}
@pytest.mark.usefixtures("get_data")
def test_wrong():
# 错误:试图访问 get_data
print(get_data) # NameError
解决方案:
# 方案 1:改用普通 fixture 方式
def test_correct_1(get_data):
print(get_data) # 可以正常访问
assert get_data["key"] == "value"
# 方案 2:如果不需要返回值,保持 usefixtures
@pytest.mark.usefixtures("get_data")
def test_correct_2():
# 只执行 fixture,不访问返回值
pass
错误 2:fixture 名称拼写错误
错误代码:
import pytest
@pytest.fixture
def setup_database():
pass
@pytest.mark.usefixtures("setup_databse") # 拼写错误:databse 应该是 database
def test_wrong():
pass
解决方案:
# 仔细检查 fixture 名称
@pytest.mark.usefixtures("setup_database") # 正确
def test_correct():
pass
错误 3:在类中使用时忘记 self 参数
错误代码:
import pytest
@pytest.fixture
def setup_data():
pass
@pytest.mark.usefixtures("setup_data")
class TestClass:
def test_method(): # 错误:缺少 self 参数
pass
解决方案:
@pytest.mark.usefixtures("setup_data")
class TestClass:
def test_method(self): # 正确:类方法必须有 self
pass
8. usefixtures 与 autouse 的对比
8.1 autouse 简介
autouse=True 是 fixture 的一个参数,表示 fixture 会自动执行,不需要在测试函数中声明。
import pytest
@pytest.fixture(autouse=True)
def auto_fixture():
"""自动执行的 fixture"""
print("自动执行 fixture")
yield
print("自动清理 fixture")
def test_example():
"""不需要声明 auto_fixture,它会自动执行"""
print("执行测试")
assert True
8.2 usefixtures 与 autouse 的对比
| 特性 | usefixtures | autouse |
|---|---|---|
| 控制范围 | 精确控制哪些测试使用 | 所有测试都使用(除非排除) |
| 灵活性 | 可以选择性应用 | 全局应用,不够灵活 |
| 使用方式 | 装饰器标记 | fixture 参数 |
| 适用场景 | 部分测试需要 | 所有测试都需要 |
| 可读性 | 明确显示哪些测试使用 | 需要查看 fixture 定义才知道 |
8.3 使用场景对比
场景 1:部分测试需要(使用 usefixtures)
import pytest
@pytest.fixture
def setup_database():
print("初始化数据库")
yield
print("清理数据库")
# 只有部分测试需要数据库
@pytest.mark.usefixtures("setup_database")
def test_with_db():
"""需要数据库的测试"""
pass
def test_without_db():
"""不需要数据库的测试"""
pass
场景 2:所有测试都需要(使用 autouse)
import pytest
@pytest.fixture(autouse=True)
def setup_logging():
"""所有测试都需要日志"""
print("配置日志")
yield
print("关闭日志")
# 所有测试都会自动使用 setup_logging
def test_1():
"""自动使用 setup_logging"""
pass
def test_2():
"""自动使用 setup_logging"""
pass
场景 3:混合使用
import pytest
@pytest.fixture(autouse=True)
def setup_logging():
"""所有测试都需要日志(autouse)"""
print("配置日志")
yield
print("关闭日志")
@pytest.fixture
def setup_database():
"""只有部分测试需要数据库(usefixtures)"""
print("初始化数据库")
yield
print("清理数据库")
# 自动使用 setup_logging,手动使用 setup_database
@pytest.mark.usefixtures("setup_database")
def test_with_db():
"""会同时使用 setup_logging 和 setup_database"""
pass
def test_without_db():
"""只使用 setup_logging"""
pass
9. 实际项目中的最佳实践
9.1 实践 1:分层使用 fixture
import pytest
# 基础层:所有测试都需要
@pytest.fixture(autouse=True)
def setup_logging():
"""日志配置(所有测试都需要)"""
import logging
logging.basicConfig(level=logging.INFO)
yield
logging.shutdown()
# 功能层:部分测试需要
@pytest.fixture
def setup_database():
"""数据库设置(部分测试需要)"""
print("连接数据库")
yield
print("关闭数据库")
@pytest.fixture
def setup_cache():
"""缓存设置(部分测试需要)"""
print("初始化缓存")
yield
print("清理缓存")
# 使用层:按需组合
@pytest.mark.usefixtures("setup_database")
class TestDatabaseOperations:
"""数据库操作测试类"""
pass
@pytest.mark.usefixtures("setup_database", "setup_cache")
class TestComplexOperations:
"""复杂操作测试类"""
pass
9.2 实践 2:使用 conftest.py 集中管理
conftest.py:
import pytest
@pytest.fixture
def setup_database():
"""数据库设置"""
print("连接数据库")
yield
print("关闭数据库")
@pytest.fixture
def setup_cache():
"""缓存设置"""
print("初始化缓存")
yield
print("清理缓存")
@pytest.fixture
def setup_api():
"""API 设置"""
print("初始化 API 客户端")
yield
print("关闭 API 客户端")
test_user.py:
import pytest
# 从 conftest.py 导入 fixture
@pytest.mark.usefixtures("setup_database", "setup_cache")
class TestUser:
def test_create_user(self):
pass
def test_delete_user(self):
pass
test_order.py:
import pytest
# 从 conftest.py 导入 fixture
@pytest.mark.usefixtures("setup_database", "setup_api")
class TestOrder:
def test_create_order(self):
pass
def test_cancel_order(self):
pass
9.3 实践 3:使用 pytest.ini 配置
pytest.ini:
[pytest]
markers =
database: 需要数据库的测试
cache: 需要缓存的测试
api: 需要 API 的测试
conftest.py:
import pytest
@pytest.fixture
def setup_database():
print("连接数据库")
yield
print("关闭数据库")
@pytest.fixture
def setup_cache():
print("初始化缓存")
yield
print("清理缓存")
test_example.py:
import pytest
# 使用 mark 和 usefixtures 组合
@pytest.mark.database
@pytest.mark.usefixtures("setup_database")
class TestDatabase:
pass
@pytest.mark.cache
@pytest.mark.usefixtures("setup_cache")
class TestCache:
pass
@pytest.mark.database
@pytest.mark.cache
@pytest.mark.usefixtures("setup_database", "setup_cache")
class TestBoth:
pass
9.4 实践 4:条件性使用 fixture
import pytest
import os
@pytest.fixture
def setup_production_db():
"""生产数据库设置"""
if os.environ.get("ENV") != "production":
pytest.skip("不在生产环境,跳过")
print("连接生产数据库")
yield
print("关闭生产数据库")
@pytest.fixture
def setup_test_db():
"""测试数据库设置"""
print("连接测试数据库")
yield
print("关闭测试数据库")
# 根据环境选择不同的 fixture
@pytest.mark.usefixtures("setup_test_db")
class TestInTestEnv:
"""测试环境"""
pass
@pytest.mark.usefixtures("setup_production_db")
class TestInProdEnv:
"""生产环境(需要特定环境变量)"""
pass
10. 完整示例:实际项目场景
10.1 示例:Web API 测试项目
项目结构:
project/
├── conftest.py
├── test_user_api.py
├── test_order_api.py
└── test_payment_api.py
conftest.py:
import pytest
import requests
from unittest.mock import Mock
@pytest.fixture(autouse=True)
def setup_logging():
"""所有测试都需要日志"""
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
yield
logging.shutdown()
@pytest.fixture
def setup_test_server():
"""启动测试服务器"""
print("启动测试服务器...")
server_url = "http://localhost:8000"
# 实际项目中这里会启动服务器
yield server_url
print("关闭测试服务器...")
@pytest.fixture
def setup_mock_external_api():
"""Mock 外部 API"""
with pytest.mock.patch('requests.get') as mock_get:
mock_response = Mock()
mock_response.status_code = 200
mock_response.json.return_value = {"status": "ok"}
mock_get.return_value = mock_response
yield
@pytest.fixture
def setup_database():
"""初始化测试数据库"""
print("连接测试数据库...")
# 创建测试表、插入测试数据等
yield
print("清理测试数据库...")
# 删除测试数据、删除测试表等
@pytest.fixture
def setup_cache():
"""初始化缓存"""
print("初始化 Redis 缓存...")
yield
print("清理 Redis 缓存...")
@pytest.fixture
def admin_token():
"""返回管理员 token(需要返回值)"""
return "admin_token_12345"
@pytest.fixture
def user_token():
"""返回用户 token(需要返回值)"""
return "user_token_67890"
test_user_api.py:
import pytest
import requests
@pytest.mark.usefixtures("setup_test_server", "setup_database", "setup_cache")
class TestUserAPI:
"""用户 API 测试类"""
def test_create_user(self, admin_token):
"""创建用户(需要 admin_token 返回值)"""
headers = {"Authorization": f"Bearer {admin_token}"}
# setup_test_server, setup_database, setup_cache 自动执行
# admin_token 需要返回值,所以用普通方式
response = requests.post(
"http://localhost:8000/api/users",
json={"name": "张三", "email": "zhangsan@example.com"},
headers=headers
)
assert response.status_code == 201
def test_get_user(self, user_token):
"""获取用户信息"""
headers = {"Authorization": f"Bearer {user_token}"}
response = requests.get(
"http://localhost:8000/api/users/1",
headers=headers
)
assert response.status_code == 200
def test_delete_user(self, admin_token):
"""删除用户"""
headers = {"Authorization": f"Bearer {admin_token}"}
response = requests.delete(
"http://localhost:8000/api/users/1",
headers=headers
)
assert response.status_code == 204
test_order_api.py:
import pytest
import requests
@pytest.mark.usefixtures("setup_test_server", "setup_database")
class TestOrderAPI:
"""订单 API 测试类(不需要缓存)"""
def test_create_order(self, user_token):
"""创建订单"""
headers = {"Authorization": f"Bearer {user_token}"}
response = requests.post(
"http://localhost:8000/api/orders",
json={"product_id": 1, "quantity": 2},
headers=headers
)
assert response.status_code == 201
def test_get_order(self, user_token):
"""获取订单信息"""
headers = {"Authorization": f"Bearer {user_token}"}
response = requests.get(
"http://localhost:8000/api/orders/1",
headers=headers
)
assert response.status_code == 200
test_payment_api.py:
import pytest
import requests
@pytest.mark.usefixtures("setup_test_server", "setup_database", "setup_mock_external_api")
class TestPaymentAPI:
"""支付 API 测试类(需要 Mock 外部 API)"""
def test_process_payment(self, user_token):
"""处理支付(外部支付 API 已被 Mock)"""
headers = {"Authorization": f"Bearer {user_token}"}
response = requests.post(
"http://localhost:8000/api/payments",
json={"order_id": 1, "amount": 100.00},
headers=headers
)
assert response.status_code == 200
# 外部 API 调用已被 Mock,不会真正调用外部服务
10.2 运行示例
运行所有测试:
pytest
运行特定测试类:
pytest test_user_api.py::TestUserAPI