Pytest 夹具(Fixture)
一、什么是夹具(Fixture)
夹具是 pytest 中用于提供测试所需资源(数据、对象、环境等)的函数。它可以帮助你:
- 避免重复代码
- 管理测试依赖
- 控制测试执行顺序
- 自动清理资源
二、基础使用
1. 定义和调用夹具
import pytest# 定义夹具
@pytest.fixture
def sample_data():return {"name": "Alice", "age": 30}# 使用夹具
def test_user_info(sample_data):assert sample_data["name"] == "Alice"assert sample_data["age"] == 30
2. 夹具作用域
# 函数级(默认)- 每个测试函数运行一次
@pytest.fixture(scope="function")
def db_connection():conn = create_connection()yield connconn.close()# 类级 - 每个测试类运行一次
@pytest.fixture(scope="class")
def class_data():return {"class_id": 101}# 模块级 - 每个模块运行一次
@pytest.fixture(scope="module")
def module_data():return {"module": "test_module"}# 会话级 - 整个测试会话运行一次
@pytest.fixture(scope="session")
def global_config():return {"env": "test"}
三、高级特性
1. 夹具依赖
@pytest.fixture
def user():return {"id": 1, "name": "Bob"}@pytest.fixture
def user_with_profile(user):profile = {"email": "bob@example.com"}user.update(profile)return userdef test_user_profile(user_with_profile):assert user_with_profile["email"] == "bob@example.com"assert user_with_profile["name"] == "Bob"
2. 参数化夹具
@pytest.fixture(params=[1, 2, 3])
def number(request):return request.paramdef test_numbers(number):assert number in [1, 2, 3]print(f"Testing with: {number}")# 这个测试会运行3次
3. 使用 yield 实现 teardown
@pytest.fixture
def database():# Setupdb = Database()db.connect()print("Database connected")yield db # 提供夹具值# Teardowndb.close()print("Database closed")def test_database(database):database.insert("data")assert database.count() == 1
4. 自动使用的夹具
@pytest.fixture(autouse=True)
def setup_teardown():print("\nBefore test")yieldprint("After test")# 每个测试都会自动执行这个夹具
四、实战示例
示例1:Web应用测试
import pytest
from fastapi.testclient import TestClient@pytest.fixture(scope="session")
def app():from main import appreturn app@pytest.fixture(scope="module")
def client(app):with TestClient(app) as test_client:yield test_client@pytest.fixture
def auth_client(client):# 创建认证后的客户端response = client.post("/login", json={"username": "testuser","password": "testpass"})token = response.json()["token"]client.headers.update({"Authorization": f"Bearer {token}"})return clientdef test_get_user(auth_client):response = auth_client.get("/user/me")assert response.status_code == 200assert response.json()["username"] == "testuser"
示例2:数据库测试
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker@pytest.fixture(scope="session")
def engine():engine = create_engine("sqlite:///:memory:")Base.metadata.create_all(engine)yield engineBase.metadata.drop_all(engine)@pytest.fixture
def db_session(engine):connection = engine.connect()transaction = connection.begin()Session = sessionmaker(bind=connection)session = Session()yield sessionsession.close()transaction.rollback()connection.close()def test_create_user(db_session):user = User(name="Alice")db_session.add(user)db_session.commit()assert db_session.query(User).count() == 1
示例3:文件操作测试
import pytest
import tempfile
import os@pytest.fixture
def temp_file():# 创建临时文件fd, path = tempfile.mkstemp()yield path# 清理os.close(fd)os.unlink(path)def test_file_operations(temp_file):with open(temp_file, 'w') as f:f.write("test data")with open(temp_file, 'r') as f:content = f.read()assert content == "test data"
五、最佳实践
1. 使用 conftest.py 共享夹具
# conftest.py
import pytest@pytest.fixture
def shared_fixture():return "shared across tests"
2. 夹具命名规范
# 好的命名
@pytest.fixture
def database_connection(): # 清晰的名称pass# 不好的命名
@pytest.fixture
def db(): # 太简短,不够清晰pass
3. 使用 fixture 工厂
@pytest.fixture
def make_user():def _make_user(name, age):return {"name": name, "age": age}return _make_userdef test_users(make_user):user1 = make_user("Alice", 30)user2 = make_user("Bob", 25)assert user1["name"] == "Alice"assert user2["name"] == "Bob"
4. 使用 pytest.mark.usefixtures
@pytest.mark.usefixtures("setup_db", "cleanup_files")
class TestUserAPI:def test_create_user(self):# 自动使用 setup_db 和 cleanup_files 夹具passdef test_delete_user(self):pass
六、调试技巧
# 1. 查看夹具执行顺序
pytest --setup-show# 2. 查看所有可用夹具
pytest --fixtures# 3. 查看特定夹具
pytest --fixtures -v | grep fixture_name# 4. 在夹具中添加调试信息
@pytest.fixture
def debug_fixture():print("Setting up")yieldprint("Tearing down")
七、常见陷阱
1. 修改不可变对象
# 错误:修改共享的不可变对象
@pytest.fixture(scope="module")
def shared_list():return [] # 这个列表会在所有测试间共享def test_append(shared_list):shared_list.append(1) # 会影响其他测试# 正确:使用函数作用域
@pytest.fixture
def fresh_list():return []
2. 异步夹具
import pytest
import asyncio@pytest.fixture
async def async_fixture():# pytest-asyncio 插件支持result = await some_async_function()return result@pytest.mark.asyncio
async def test_async(async_fixture):assert await async_fixture.process()
八、总结
Pytest 夹具的核心要点:
- 作用域管理:合理选择 scope 避免资源浪费
- 依赖注入:通过参数自动注入
- 资源清理:使用 yield 确保 teardown
- 代码复用:通过 conftest.py 共享夹具
- 灵活配置:支持参数化和工厂模式
掌握这些概念后,你可以编写更简洁、可维护的测试代码。