如何从另一个 Poetry 包加载 pytest fixtures?
通过 pytest_plugins 可以将公共包中的 fixture 模块注册为 pytest 插件:
pytest_plugins = ["common.tests.db_conf"]
这个加载方式是正确的。只要测试环境能导入该模块,且 pytest 加载了声明它的 conftest.py,db_session 就应当可用。Poetry 负责安装包,pytest 负责注册 fixtures,两者之间不需要额外的专用配置。
仅凭目前的代码和报错还无法确定根因,需要检查 Docker 容器实际使用的文件和依赖版本。
1. 在项目顶层注册公共 fixtures
建议将插件声明统一放在 user-service/conftest.py 中,与 pyproject.toml 同级:
user-service/
├── pyproject.toml
├── conftest.py
├── user_service/
│ └── ...
└── tests/
├── conftest.py
└── test_users.py
user-service/conftest.py:
pytest_plugins = ["common.tests.db_conf"]
pytest 会将这个模块作为插件加载,注册其中的 test_engine、setup_db、db_session 等 fixtures。将声明放在顶层,也能避免后续收集测试时遇到非顶层 conftest.py 的插件加载限制。参见 pytest 插件加载说明。
然后删除 tests/conftest.py 中重复的 pytest_plugins 声明,以及这段显式导入:
from common.tests.db_conf import (
anyio_backend,
test_engine,
setup_db,
db_session,
)
直接导入 fixture 通常也能完成注册,但 pytest 推荐通过插件机制复用其他项目的 fixtures,无须同时使用这两种方式。参见 跨项目复用 fixtures。
tests/conftest.py 只保留服务自己的 HTTP client:
import pytest
from httpx import ASGITransport, AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
from common.core.database import get_db
from user_service.main import app
@pytest.fixture
async def client(db_session: AsyncSession):
async def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
try:
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://users_test",
) as ac:
yield ac
finally:
app.dependency_overrides.pop(get_db, None)
通过参数名 db_session 就能请求公共 fixture,无须再次导入。client 清理时,finally 会移除对应的依赖覆盖。
2. 确认公共模块包含在安装包中
公共项目可以采用以下结构:
common/
├── pyproject.toml
└── common/
├── __init__.py
├── core/
├── models/
└── tests/
├── __init__.py
└── db_conf.py
外层 common/ 是项目目录,内层 common/ 是 Python 包,对应的导入路径为:
common.tests.db_conf
这里采用普通 Python 包结构,保留 __init__.py 便于明确包的边界。Python 也支持命名空间包,因此不能仅凭缺少这个文件就认定它是报错根因。
可以在公共项目的 pyproject.toml 中明确指定打包目录:
[tool.poetry]
# 保留已有的 name、version 等配置
packages = [{ include = "common" }]
名称和目录符合约定时,Poetry 可以自动发现包。还要检查已有的 exclude、include 等配置,确认发布的 wheel 包含 common/tests/db_conf.py。宿主机中有这个文件,不代表容器安装的发行包也包含它。参见 Poetry 包配置。
3. 在 Docker 的实际测试环境中排查
在 users_test 容器的项目工作目录下执行以下命令。如果测试使用 Poetry 环境,先检查解释器和模块来源:
poetry run python -c 'import sys; import common.tests.db_conf as m; print(sys.executable); print(m.__file__); print(hasattr(m, "db_session"))'
检查结果应满足:
- 导入没有报错。
m.__file__指向预期的公共包。- 最后一行输出为
True。
导入失败时,先排查包安装、文件打包或模块依赖问题。路径指向旧版本或意外目录时,则需要检查镜像、挂载和依赖安装过程。
接着检查 pytest 实际加载的配置与 fixtures:
poetry run python -m pytest --trace-config --collect-only tests
poetry run python -m pytest --fixtures -v tests
第一条命令检查顶层 conftest.py 和 common.tests.db_conf 是否已加载;第二条确认 db_session 是否已注册,并显示它的定义位置。
原始错误中的 anyio_backend 也可能来自 AnyIO 自带的插件,不能据此判断 common.tests.db_conf 已加载。如果插件声明已执行,但目标模块无法导入,通常会直接报插件导入错误。因此,排查“找不到 fixture”时,还需要结合这些加载记录。
你的依赖配置同时包含:
[tool.poetry.dependencies]
common = "^0.1.1"
[tool.poetry.group.local.dependencies]
common = { path = "../common", develop = true }
local 分组名不会自动切换依赖来源。需要检查 poetry.lock 和 Dockerfile 中实际执行的安装命令,确认容器使用的依赖来源符合预期。
使用路径依赖时,容器内必须存在对应路径。使用已发布版本时,修改本地公共包不会更新已发布的包。如果构建镜像时安装了旧版本,还需要更新依赖并重新构建镜像。
4. 将异步执行配置与 fixture 注册分开处理
现有配置:
[tool.pytest.ini_options]
asyncio_mode = "auto"
会让 pytest-asyncio 接管使用 @pytest.fixture 定义的异步 fixtures。仅为解决跨包加载问题,无须把所有装饰器改成 @pytest_asyncio.fixture。参见 pytest-asyncio 的 auto 模式。
如果继续使用这种方式,就由 pytest-asyncio 负责这些异步测试,避免再添加 AnyIO 标记,让两套插件处理同一批测试。公共模块中的:
pytest_plugins = ["anyio"]
也不负责注册 db_session。正常启用插件自动加载时,安装 AnyIO 就会提供它的 pytest 插件。
如果选择由 AnyIO 执行测试,应将 pytest-asyncio 设为 strict 模式,并按 AnyIO 的要求标记测试。对于 setup_db 这样的 session 级异步 fixture,还要保留相应作用域的 anyio_backend。参见 AnyIO 测试说明。
这些配置决定异步 fixture 如何执行。先通过 --fixtures 确认 db_session 已注册,再处理可能出现的事件循环或数据库初始化错误。
备注:内容仅供参考。
