Python中如何导入tests子模块且能单独运行子目录测试?
问题描述
测试目录结构如下:
tests ├── __init__.py ├── conftest.py ├── data_validation │ └── test_data_validation.py └── per_vendor ├── __init__.py ├── vendor_a │ ├── conftest.py │ └── test_vendor_a.py └── vendor_b ├── __init__.py ├── conftest.py └── test_vendor_b.py
在tests/conftest.py中编写了以下fixture代码:
import pytest from tests import per_vendor @pytest.fixture def database_with_data(empty_database: Engine) -> Engine: helpers = [ per_vendor.vendor_a.conftest.fill_with_data_for_a, per_vendor.vendor_b.conftest.fill_with_data_for_b, ] for cur in helpers: cur(empty_database) return empty_database
其中empty_database是提供空数据库的fixture。
运行全量测试python -m pytest tests/时一切正常,但单独运行特定子目录测试(如python -m pytest tests/data_validation/)时,所有依赖database_with_data fixture的测试会报错:
AttributeError: module 'tests.per_vendor' has no attribute 'vendor_a'
已知tests.per_vendor可以被正常导入,但它的子模块无法被识别,需要了解差异原因并找到支持模块化配置且能单独运行子目录测试的解决方案。
原因分析
- 全量测试时,pytest会扫描所有测试文件,包括
per_vendor/vendor_a和per_vendor/vendor_b下的测试,这会触发对应子模块的导入,因此per_vendor模块会自动包含vendor_a、vendor_b这些子模块属性。 - 仅运行
tests/data_validation/目录的测试时,pytest不会扫描per_vendor下的测试文件,Python解释器也不会自动导入per_vendor的子模块。此时from tests import per_vendor只会导入per_vendor/__init__.py对应的模块,而不会加载其子模块,所以访问per_vendor.vendor_a会抛出属性不存在的错误。
解决方案
提供三种可行的解决思路,可根据项目结构选择:
方案1:显式导入子模块
修改tests/per_vendor/__init__.py文件,显式导入所有子模块:
from . import vendor_a from . import vendor_b
这样无论是否运行per_vendor下的测试,只要导入per_vendor模块,就会自动加载vendor_a和vendor_b子模块,解决属性不存在的问题。
方案2:动态导入子模块
在database_with_data fixture中使用动态导入,避免依赖预加载的子模块:
import pytest import importlib @pytest.fixture def database_with_data(empty_database: Engine) -> Engine: helper_specs = [ ("tests.per_vendor.vendor_a.conftest", "fill_with_data_for_a"), ("tests.per_vendor.vendor_b.conftest", "fill_with_data_for_b") ] for module_path, func_name in helper_specs: mod = importlib.import_module(module_path) func = getattr(mod, func_name) func(empty_database) return empty_database
这种方式会在fixture执行时动态导入所需的模块和函数,不受测试运行范围的影响。
方案3:重构fixture结构(推荐)
将数据填充逻辑与vendor绑定,利用pytest的fixture自动发现机制:
- 在
per_vendor/vendor_a/conftest.py中定义针对vendor_a的fixture:
import pytest def fill_with_data_for_a(empty_database): # 原有填充逻辑 pass @pytest.fixture def database_with_vendor_a_data(empty_database: Engine) -> Engine: fill_with_data_for_a(empty_database) return empty_database
- 在
per_vendor/vendor_b/conftest.py中定义针对vendor_b的fixture:
import pytest def fill_with_data_for_b(empty_database): # 原有填充逻辑 pass @pytest.fixture def database_with_vendor_b_data(empty_database: Engine) -> Engine: fill_with_data_for_b(empty_database) return empty_database
- 在
tests/conftest.py中合并这两个fixture:
import pytest @pytest.fixture def database_with_data(database_with_vendor_a_data, database_with_vendor_b_data) -> Engine: # 两个fixture已依次执行数据填充,直接返回最终数据库实例 return database_with_vendor_a_data
这种方式更符合pytest的模块化设计,每个vendor的逻辑独立,且pytest会自动根据测试依赖加载对应的fixture,无论运行哪个目录的测试都能正常工作。
内容的提问来源于stack exchange,提问作者Max Görner
相关产品推荐
相关产品推荐

