PAYATHON 2026

如何从另一个 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 已注册,再处理可能出现的事件循环或数据库初始化错误。

备注:内容仅供参考。