夹具函数

夹具函数使用@pytest.fixture 装饰器:

 

详细解释

1. @pytest.fixture 装饰器的作用

python
@pytest.fixture  # 这个装饰器告诉 pytest:这是一个 fixture
def db_connection():
    """创建测试数据库连接"""
    conn = sqlite3.connect(':memory:')
    yield conn      # 暂停点:把 conn 提供给测试函数
    conn.close()    # 恢复点:测试结束后继续执行

@pytest.fixture 装饰器将普通函数标记为 pytest 的 fixture,使其具有特殊的能力。

2. yield conn 的含义

yield conn 做了两件事:

  1. 提供资源:将 conn(数据库连接对象)传递给使用这个 fixture 的测试函数

  2. 暂停执行:函数在 yield 处暂停,等待测试函数执行完毕

执行流程演示

python
# tests/conftest.py
import pytest
import sqlite3

@pytest.fixture
def db_connection():
    """创建测试数据库连接"""
    print("\n1. 开始创建数据库连接")
    conn = sqlite3.connect(':memory:')
    print("2. 数据库连接已创建")
    
    yield conn  # 在这里暂停,把 conn 交给测试函数
    
    print("4. 测试结束,开始清理")
    conn.close()
    print("5. 数据库连接已关闭")

@pytest.fixture
def db_cursor(db_connection):
    """创建数据库游标"""
    print("3. 创建游标")
    cursor = db_connection.cursor()
    
    yield cursor  # 把 cursor 交给测试函数
    
    print("6. 关闭游标")
    cursor.close()
python
# tests/test_example.py
def test_database(db_cursor):
    print(">>> 测试函数执行中")
    db_cursor.execute("SELECT 1")
    result = db_cursor.fetchone()
    assert result == (1,)
    print(">>> 测试函数执行完毕")

 

运行时的输出顺序:

text
1. 开始创建数据库连接
2. 数据库连接已创建
3. 创建游标
>>> 测试函数执行中
>>> 测试函数执行完毕
6. 关闭游标
4. 测试结束,开始清理
5. 数据库连接已关闭

 流程解释:

db_cursor fixture 声明依赖 db_connection 时,pytest 会:

  1. 先执行 db_connection fixture 函数

  2. 获取 yield conn 中 yield 出来的值

  3. 将这个值作为参数传递给 db_cursor

@pytest.fixture
def db_connection():
    conn = sqlite3.connect(':memory:')  # conn 是 sqlite3.Connection 对象
    yield conn  # yield 出去的是 sqlite3.Connection 对象

@pytest.fixture
def db_cursor(db_connection):  # 参数 db_connection 接收的是 sqlite3.Connection 对象
    cursor = db_connection.cursor()  # 调用 Connection 对象的 cursor() 方法
    yield cursor
    cursor.close()

 

完整的执行流程

# 步骤分解
@pytest.fixture
def db_connection():
    # 第1步:执行到这里
    conn = sqlite3.connect(':memory:')
    
    # 第2步:yield 暂停,把 conn(Connection对象)传递给依赖它的 fixture 或测试
    yield conn
    
    # 第5步:测试结束后,从 yield 处恢复,执行清理
    conn.close()

@pytest.fixture
def db_cursor(db_connection):
    # 第3步:db_connection 参数 = conn(Connection对象)
    # 调用 Connection 对象的 cursor() 方法创建游标
    cursor = db_connection.cursor()
    
    # 第4步:yield 暂停,把 cursor(Cursor对象)传递给测试函数
    yield cursor
    
    # 第6步:测试结束后,关闭游标
    cursor.close()
 

夹具函数的作用:

夹具函数的主要作用

1. 代码复用(最直接的作用)

python
# 没有夹具:每个测试都要重复
def test_user_create():
    conn = sqlite3.connect(':memory:')
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)")
    # ... 测试逻辑
    cursor.close()
    conn.close()

def test_user_update():
    conn = sqlite3.connect(':memory:')  # 重复!
    cursor = conn.cursor()  # 重复!
    cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)")  # 重复!
    # ... 测试逻辑
    cursor.close()  # 重复!
    conn.close()  # 重复!

# 使用夹具:一次定义,多处使用
@pytest.fixture
def db_cursor():
    conn = sqlite3.connect(':memory:')
    cursor = conn.cursor()
    cursor.execute("CREATE TABLE users (id INTEGER, name TEXT)")
    yield cursor
    cursor.close()
    conn.close()

def test_user_create(db_cursor):
    # 直接使用,无需重复
    pass

def test_user_update(db_cursor):
    # 直接使用,无需重复
    pass

 

2. 资源管理和清理

@pytest.fixture
def temp_file():
    # 创建资源
    file = tempfile.NamedTemporaryFile(delete=False)
    file.write(b"test data")
    file.close()
    
    yield file.name
    
    # 自动清理
    os.unlink(file.name)

@pytest.fixture
def mock_server():
    # 启动服务
    server = start_server()
    
    yield server
    
    # 停止服务
    server.stop()

 

3. 依赖管理

@pytest.fixture
def app():
    """基础应用实例"""
    app = create_app()
    return app

@pytest.fixture
def client(app):
    """测试客户端,依赖 app"""
    return app.test_client()

@pytest.fixture
def auth_client(client):
    """已认证的客户端,依赖 client"""
    client.login(username="test", password="test")
    return client

 

4. 测试隔离

@pytest.fixture
def isolated_db():
    """每个测试都有独立的数据库"""
    conn = sqlite3.connect(':memory:')  # 内存数据库,自动隔离
    yield conn
    conn.close()

def test_case1(isolated_db):
    isolated_db.execute("INSERT INTO users VALUES (1, 'Alice')")
    # 只影响自己的数据库

def test_case2(isolated_db):
    # 看不到 test_case1 的数据
    result = isolated_db.execute("SELECT COUNT(*) FROM users").fetchone()
    assert result[0] == 0  # 表是空的

 

复用范围的控制

1. 不同作用域的复用

# session 级别:整个测试会话复用一次
@pytest.fixture(scope="session")
def expensive_resource():
    """耗时操作只执行一次"""
    print("创建昂贵的资源(只执行一次)")
    resource = create_expensive_resource()
    return resource

# module 级别:每个测试模块复用一次
@pytest.fixture(scope="module")
def module_db():
    """模块内所有测试共享一个数据库连接"""
    conn = sqlite3.connect(':memory:')
    yield conn
    conn.close()

# function 级别:每个测试函数独立
@pytest.fixture(scope="function")  # 默认
def fresh_db():
    """每个测试获取新的数据库"""
    conn = sqlite3.connect(':memory:')
    yield conn
    conn.close()

2. conftest.py 的层级复用

 
project/
├── conftest.py          # 全局夹具,所有测试可用
├── tests/
│   ├── conftest.py      # tests/ 目录的夹具
│   ├── test_user.py
│   └── api/
│       ├── conftest.py  # 只影响 api/ 目录
│       └── test_api.py
# project/conftest.py - 全局
@pytest.fixture
def global_config():
    return load_global_config()

# tests/conftest.py - tests 目录
@pytest.fixture
def db_connection():
    return create_db()

# tests/api/conftest.py - api 目录专用
@pytest.fixture
def api_client():
    return APIClient()

 

夹具复用 vs 普通函数复用

普通函数方式

def create_test_db():
    """普通辅助函数"""
    conn = sqlite3.connect(':memory:')
    cursor = conn.cursor()
    return conn, cursor

def test_user_create():
    conn, cursor = create_test_db()
    try:
        # 测试逻辑
        pass
    finally:
        cursor.close()
        conn.close()  # 手动清理,容易忘记

def test_user_update():
    conn, cursor = create_test_db()  # 复用了创建逻辑
    try:
        # 测试逻辑
        pass
    finally:
        cursor.close()  # 但清理还是要手动
        conn.close()

 

夹具方式

@pytest.fixture
def db_cursor():
    """夹具:创建+清理一体"""
    conn = sqlite3.connect(':memory:')
    cursor = conn.cursor()
    yield cursor
    cursor.close()  # 自动清理
    conn.close()

def test_user_create(db_cursor):
    # 只需要声明依赖,自动创建和清理
    pass

def test_user_update(db_cursor):
    # 同样自动处理
    pass

实际案例:不同层级的复用

 
# 基础夹具:可被多个模块复用
@pytest.fixture
def app():
    """创建应用实例"""
    app = create_app(config='test')
    return app

@pytest.fixture
def db(app):
    """基于应用创建数据库"""
    with app.app_context():
        db = create_db(app.config['DATABASE'])
        yield db
        db.drop_all()

# 用户模块复用
def test_create_user(app, db):
    user = User(name='Alice')
    db.session.add(user)
    db.session.commit()
    assert user.id is not None

# 订单模块也复用
def test_create_order(app, db):
    order = Order(user_id=1, product='Book')
    db.session.add(order)
    db.session.commit()
    assert order.id is not None

# 完全不同的测试也能复用
def test_app_config(app):
    assert app.config['TESTING'] is True

总结

夹具函数的作用确实是代码复用,但这种复用包含了:

  1. 创建逻辑的复用:不用重复写初始化代码

  2. 清理逻辑的复用:自动处理资源释放

  3. 依赖关系的复用:自动处理夹具间的依赖

  4. 配置的复用:统一的测试环境配置

夹具让代码复用更加系统化和自动化,而不仅仅是提取公共函数那么简单。




 
 
 
 
posted @ 2026-09-09 13:44  苹果芒  阅读(4)  评论(0)    收藏  举报