【pytest】(九)conftest.py是什么?

如果第三方插件满足不了自己的需求,那么就需要你自己定制化开发自己的插件了。

正因如此,pytest的插件生态非常的丰富,一般的常规性需求基本上都可以找到现成的第三方插件。

说到开发插件的话,还是要引入一个新的名词hook functions ,对于钩子函数是要另起一篇介绍的,那也是真正开发插件的开始。

一个插件里包含了一个或者多个钩子函数,编写钩子函数可以实现功能丰富的插件,所以pytest框架从配置、收集用例到运行用例,再到最后出报告,都是通过调用各种插件来实现的。

目前所说插件就是以下这3种了:

  • 内置插件,就是内部_pytest这个目录下的模块(.\Python37\Lib\site-packages_pytest)
  • 外部插件,就是你安装的第三方插件
  • conftest.py ,这个可以理解成框架的固定写法,把hook或者fixture写在这个文件里,就会自动去调用。

一、conftest.py

内置插件、外部插件都还是很好理解的,那conftest文件是什么?
其实,它是一个python文件。
开个玩笑,其实conftest.py通常可以用来做2个事情:

  1. 存放你的fixture函数
  2. 在里面写自己的本地插件

另外,可以根据conftest.py的文件存放位置,来决定它的适用范围。

二、conftest.py放在哪

<think>我们被要求回答关于如何将pytestconftest.py按模块拆分和使用的问题。根据引用内容,我们可以总结出以下信息: 1. 引用[2]提到可以将conftest.py中的fixtures拆分到多个文件中,并给出了一个目录结构示例: ``` │ ├── fixtures │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── loop.py │ │ ├── net.py │ │ ├── request.py │ │ ├── signal.py │ │ └── thread.py ``` 并且还提供了一个函数`append_pytest_fixture_plugins`来动态添加这些fixture文件。 2. 引用[3]说明了conftest.py的一些特性:它可以跨文件调用,且不需要导入,pytest会自动发现;一个项目可以有多个conftest.py文件,子目录的conftest.py可以覆盖父目录的同名fixture。 3. 引用[4]展示了conftest.py中定义fixture的示例。 根据这些信息,我们可以设计一个按模块拆分conftest.py的方法: ### 方法一:使用多个conftest.py文件 由于pytest支持在多个目录层级中放置conftest.py文件,我们可以为不同的模块(子目录)创建各自的conftest.py文件。这样,每个模块的测试可以使用自己模块的fixture,同时也可以使用父目录的fixture(如果有同名,则子目录的fixture会覆盖父目录的)。 例如,项目结构如下: ``` tests/ ├── conftest.py # 全局的fixture ├── test_module1/ │ ├── conftest.py # module1专用的fixture │ └── test_xxx.py └── test_module2/ ├── conftest.py # module2专用的fixture └── test_yyy.py ``` ### 方法二:将fixture拆分到多个文件,然后在一个conftest.py中导入 但是,有时我们希望在一个conftest.py文件中管理多个模块的fixture,但又不想让这个文件变得太大。这时,我们可以将fixture定义拆分到多个文件中,然后在conftest.py中导入这些文件。 然而,引用[3]提到“conftest.py文件不能被其他文件导入”,所以我们不能直接在其他文件中定义fixture然后导入到conftest.py中?实际上,引用[2]提供了一种方法:将fixture定义在普通Python文件中,然后通过pytest的插件机制来加载。 具体步骤: 1. 创建一个目录(例如`fixtures`)来存放各个模块的fixture文件,每个文件包含一组相关的fixture。 2. 在`fixtures`目录下,每个fixture文件都是一个Python模块(.py文件),并且需要确保每个文件都定义了pytest的fixture。 3. 然后,我们需要告诉pytest去加载这些fixture文件。引用[2]中提供了一个函数`append_pytest_fixture_plugins`,它的作用是将指定目录下的所有Python文件作为pytest的插件进行加载。具体做法是在conftest.py中调用这个函数,将fixture文件的路径转换为插件格式(即模块路径)并添加到pytest的插件列表中。 例如,在项目根目录的conftest.py中: ```python from pathlib import Path import pytest # 假设我们有一个fixtures目录,位于tests/fixtures root_path = Path(__file__).parent # 当前conftest.py所在目录 fixtures_path = root_path / 'fixtures' # 获取所有fixture文件的模块路径(相对于项目根目录,但这里我们相对于当前conftest.py所在目录) # 注意:我们需要将fixtures目录下的所有.py文件(除了__init__.py)作为插件加载 # 我们可以使用引用[2]中的函数,稍作修改 def get_plugin_names_from_dir(directory: Path, root: Path) -> list: """获取目录下所有Python文件的插件模块名(相对root的模块路径)""" plugins = [] for file in directory.iterdir(): if file.is_file() and file.suffix == '.py' and not file.name.startswith('__'): # 计算相对于root的路径,并转换为模块格式(用点分隔,去掉.py) rel_path = file.relative_to(root) # 将路径分隔符转换为点,并去掉后缀 module_name = str(rel_path).replace('/', '.').replace('\\', '.').replace('.py', '') plugins.append(module_name) return plugins # 获取插件模块名列表 plugins = get_plugin_names_from_dir(fixtures_path, root_path) # 将每个插件模块名添加到pytest的插件列表中 for plugin in plugins: pytest_plugins = [plugin] # 注意:这里实际上是覆盖,所以我们需要累积 # 但是上面的写法会覆盖,正确做法是追加到已有的pytest_plugins列表中 ``` 然而,引用[2]中的函数是递归遍历,但这里我们只考虑单层目录。如果要递归,可以修改函数。 但是,更常见的做法是在conftest.py中直接导入这些fixture文件(尽管引用[3]说不能导入,但实际上我们可以导入,只要确保fixture被定义)。不过,pytest官方推荐使用`pytest_plugins`变量来指定要加载的插件模块(即我们的fixture模块)。 在conftest.py中,我们可以这样: ```python pytest_plugins = [ 'tests.fixtures.cache', 'tests.fixtures.loop', 'tests.fixtures.net', # ... 其他模块 ] ``` 但是,如果有很多模块,手动维护这个列表很麻烦。因此,引用[2]提供了自动化的方法,即动态生成这个列表。 ### 动态加载fixture模块 在conftest.py中,我们可以动态地扫描目录,然后将所有模块名添加到`pytest_plugins`列表中。 注意:`pytest_plugins`是一个字符串列表,每个字符串是一个模块的绝对导入路径。 例如,我们有一个`tests/fixtures`目录,里面包含多个fixture文件,那么我们可以这样: ```python import os from pathlib import Path # 获取当前conftest.py所在目录 current_dir = Path(__file__).parent # 设置fixtures目录的相对路径 fixtures_dir = current_dir / "fixtures" # 动态获取fixtures目录下所有.py文件(不包括__init__.py)的模块名 pytest_plugins = [] for root, dirs, files in os.walk(fixtures_dir): for file in files: if file.endswith(".py") and not file.startswith("__"): # 获取相对于current_dir的路径 rel_path = os.path.relpath(os.path.join(root, file), current_dir) # 将路径转换为模块导入格式(去掉.py,将路径分隔符替换为.) module_name = rel_path.replace(os.sep, ".")[:-3] pytest_plugins.append(module_name) ``` 但是,注意:这样得到的模块名是相对于`tests`目录(即current_dir)的。例如,如果有一个文件`fixtures/cache.py`,那么模块名就是`fixtures.cache`(注意,这里要求current_dir在sys.path中,或者该目录是一个包)。因此,我们需要确保`tests`目录是一个包(即包含`__init__.py`文件)。 另外,引用[2]中的函数`append_pytest_fixture_plugins`也是类似的功能,但它是从根目录开始计算相对路径。 ### 注意事项 1. 拆分后的fixture文件应该放在一个包内(即包含`__init__.py`的目录),这样Python才能将它们作为模块导入。 2. 在动态加载时,确保模块路径正确,不会出现导入错误。 ### 示例 假设项目结构如下: ``` project/ ├── tests/ │ ├── conftest.py │ ├── fixtures/ │ │ ├── __init__.py │ │ ├── cache.py │ │ └── net.py │ ├── test_module1/ │ │ └── test_example.py │ └── test_module2/ │ └── test_another.py ``` 在`tests/conftest.py`中,我们可以动态加载`fixtures`目录下的所有模块: ```python import os from pathlib import Path current_dir = Path(__file__).parent fixtures_dir = current_dir / "fixtures" pytest_plugins = [] # 遍历fixtures_dir下的所有文件 for file_path in fixtures_dir.glob('**/*.py'): # 跳过__init__.py if file_path.name.startswith('__'): continue # 获取相对于current_dir的路径 rel_path = file_path.relative_to(current_dir) # 转换为模块名:去掉.py,将路径分隔符替换为点 module_name = str(rel_path).replace(os.sep, '.')[:-3] pytest_plugins.append(module_name) ``` 这样,`cache.py`和`net.py`中的fixture就会被自动加载。 ### 总结 有两种主要方式拆分conftest.py: 1. 使用多个conftest.py文件:在需要的地方(子目录)放置conftest.pypytest会自动发现。 2. 将fixture拆分到多个文件中,然后在一个conftest.py中通过`pytest_plugins`动态导入这些文件作为插件。 动态导入的方法适用于需要集中管理大量fixture,并且希望按模块组织的情况。 §§相关问题§§ 1. 如何解决动态加载fixture模块时出现的导入错误? 2. 拆分后的fixture文件如何组织更合理? 3. 子目录的conftest.py如何覆盖父目录的fixture?</think>### 如何在 pytest 中将 conftest.py 按模块进行拆分和使用 在 pytest 中,将 `conftest.py` 按模块拆分可以提高代码可维护性,使不同功能的 fixture 更易于管理。以下是具体实现方法: #### 步骤 1:创建模块化 fixture 目录结构 ```bash tests/ ├── conftest.py # 主配置文件 └── fixtures/ # fixture 模块目录 ├── __init__.py # 标识为 Python 包 ├── database.py # 数据库相关 fixture ├── auth.py # 认证相关 fixture ├── api.py # API 相关 fixture └── utils.py # 工具类 fixture ``` #### 步骤 2:主 conftest.py 动态加载模块 ```python # tests/conftest.py import os import importlib.util def load_fixture_modules(): """动态加载所有 fixture 模块""" fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures') for file_name in os.listdir(fixture_dir): if file_name.endswith('.py') and file_name != '__init__.py': module_name = file_name[:-3] # 移除 .py 后缀 module_path = os.path.join(fixture_dir, file_name) # 动态导入模块 spec = importlib.util.spec_from_file_location( f"fixtures.{module_name}", module_path ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) # 加载所有 fixture 模块 load_fixture_modules() ``` #### 步骤 3:模块化 fixture 示例 ```python # tests/fixtures/auth.py import pytest @pytest.fixture(scope="session") def admin_token(): """管理员认证 token""" return "admin-token-123" @pytest.fixture(scope="function") def user_credentials(): """普通用户凭证""" return {"username": "test_user", "password": "pass123"} ``` ```python # tests/fixtures/database.py import pytest from myapp.database import init_db @pytest.fixture(scope="module") def test_database(): """初始化测试数据库""" db = init_db() yield db db.cleanup() ``` #### 步骤 4:在测试中使用模块化 fixture ```python # tests/api/test_login.py def test_admin_login(admin_token): headers = {"Authorization": f"Bearer {admin_token}"} # ... 测试逻辑 ... def test_user_login(user_credentials): # ... 使用凭证测试登录 ... ``` #### 关键优势: 1. **按功能拆分**:将不同领域的 fixture(数据库、认证、API等)分离到独立模块 2. **自动发现**:pytest 自动发现所有 fixture 无需手动导入[^3] 3. **作用域控制**:每个 fixture 可独立设置作用域(function/class/module/session) 4. **覆盖机制**:子目录的 fixture 可覆盖父目录同名 fixture[^1] #### 注意事项: 1. 所有 fixture 模块必须放在测试包内(包含 `__init__.py` 的目录)[^3] 2. 文件名需遵循 Python 模块命名规范(无特殊字符) 3. 使用 `pytest_plugins` 显式声明插件模块(可选): ```python # tests/conftest.py pytest_plugins = [ "fixtures.database", "fixtures.auth" ] ``` > **最佳实践**:对于大型项目,可按子系统进一步拆分: > ``` > tests/ > ├── api/ > │ ├── conftest.py > │ └── fixtures/ > ├── web/ > │ ├── conftest.py > │ └── fixtures/ > └── mobile/ > ├── conftest.py > └── fixtures/ > ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值