07.pytest的usefixture

Pytest 中的 @pytest.mark.usefixtures 详解

1. 什么是 usefixtures

1.1 基本概念

@pytest.mark.usefixtures 是 pytest 提供的一个装饰器,用于在测试类测试函数上使用 fixture,而不需要将 fixture 作为参数传递

1.2 形象比喻

想象一下,你要使用一个工具:

普通 fixture 使用方式(需要参数):

  • 就像你每次要用锤子时,都要说”给我锤子”(在函数参数中声明)
  • 然后你才能使用这个锤子

usefixtures 使用方式(不需要参数):

  • 就像你进入一个”工具房”(使用装饰器标记)
  • 工具房里已经准备好了所有工具(fixture 自动执行)
  • 你不需要明确说”给我锤子”,工具已经在那里了
  • 但你不能直接拿到工具本身(不能访问 fixture 的返回值)

1.3 usefixtures 的作用

usefixtures 主要有以下几个作用:

  1. 自动执行 fixture:不需要在测试函数参数中声明,fixture 会自动执行
  2. 简化测试函数签名:测试函数不需要添加 fixture 参数,保持函数签名简洁
  3. 批量应用 fixture:可以给整个测试类应用 fixture,类中所有测试都会自动使用
  4. 只执行副作用:适合那些只需要执行前置/后置操作,不需要返回值的 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 基本执行顺序规则

  1. fixture 的执行顺序:按照在 usefixtures 中声明的顺序执行
  2. 清理顺序:与执行顺序相反(后进先出,LIFO)
  3. 与普通 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

执行顺序说明

  1. 先执行 usefixtures 中的 fixture(按声明顺序):fixture_afixture_bfixture_c
  2. 然后执行普通参数 fixture:data_fixture
  3. 最后执行测试函数
  4. 清理顺序相反:fixture_cfixture_bfixture_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(类级别)

执行顺序说明

  1. 先执行类级别的 fixture:class_fixture
  2. 然后执行函数级别的 fixture:function_fixture
  3. 执行测试方法
  4. 清理顺序相反:先清理函数级别,再清理类级别

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

发表评论