You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

6.1 KiB

测试规范修正:移除硬编码 BASE_URL

日期: 2026-01-28
类型: 测试规范优化
影响范围: 测试框架

背景

在测试代码中发现硬编码的 BASE_URL = "http://localhost:6170/api/v1",这会导致:

  • 测试依赖特定端口号
  • 无法适配不同测试环境
  • 违反了使用 AsyncClient 的最佳实践

修正内容

1. 移除硬编码 BASE_URL

修改前

BASE_URL = "http://localhost:6170/api/v1"  # 硬编码

response = await client.get(f"{BASE_URL}/users/me")

修改后

@pytest.fixture
def api_prefix() -> str:
    """API 前缀 fixture"""
    return "/api/v1"

response = await async_client.get(f"{api_prefix}/users/me")

2. 更新 conftest.py

添加 api_prefix fixture:

@pytest.fixture
def api_prefix() -> str:
    """API 前缀 fixture"""
    return "/api/v1"

3. 更新测试文件

  • server/tests/integration/test_user_api.py:所有测试方法添加 api_prefix 参数
  • server/tests/integration/test_credit_api.py:所有测试方法添加 api_prefix 参数

4. 更新测试规范文档

.claude/skills/jointo-tech-stack/references/testing.md 中添加:

⚠️ 重要:不要硬编码 BASE_URL

错误写法(硬编码 BASE_URL):

BASE_URL = "http://localhost:6170/api/v1"  # 硬编码,依赖端口

response = await client.get(f"{BASE_URL}/users/me")

正确写法(使用 fixture 提供 API 前缀):

@pytest.fixture
def api_prefix() -> str:
    """API 前缀 fixture"""
    return "/api/v1"

@pytest.mark.asyncio
async def test_get_user(async_client: AsyncClient, api_prefix: str):
    """测试获取用户信息"""
    response = await async_client.get(f"{api_prefix}/users/me")
    assert response.status_code == 200

为什么不要硬编码 BASE_URL

  • AsyncClient 已经通过 base_url="http://test" 设置了基础 URL
  • 硬编码端口号会导致测试依赖特定环境
  • 使用 fixture 可以统一管理 API 版本前缀
  • 更容易适配不同的测试环境

5. 更新 pytest.ini

添加注释说明不要重复声明 asyncio marker:

# 标记(markers)
markers =
    integration: 集成测试(需要真实数据库、Redis)
    unit: 单元测试(使用 Mock)
    slow: 慢速测试
    # 注意:asyncio 由 pytest-asyncio 插件提供,不要重复声明

6. 修正 conftest.py 的 db_session fixture

使用正确的事务回滚写法:

修改前

async with engine.connect() as connection:
    async with connection.begin() as transaction:
        session = AsyncSession(bind=connection, ...)
        try:
            yield session
        finally:
            await session.close()
            await transaction.rollback()

修改后

async with engine.connect() as conn:
    trans = await conn.begin()
    async with AsyncSession(bind=conn, expire_on_commit=False) as session:
        yield session
    await trans.rollback()

优势

  1. 环境无关:不依赖特定端口号
  2. 统一管理:API 前缀集中在 fixture 中
  3. 易于维护:修改 API 版本只需改一处
  4. 符合最佳实践:使用 AsyncClient 的推荐方式

已知问题

测试运行时出现 RuntimeError: Task got Future attached to a different loop 错误,这是因为 AsyncClient 和 FastAPI app 使用了不同的事件循环。需要进一步修正 async_client fixture 的实现方式。

后续工作

  • 修正 async_client fixture 的事件循环问题
  • 验证所有测试能正常运行
  • 更新其他测试文件使用 api_prefix fixture

相关文件

  • server/tests/conftest.py
  • server/tests/pytest.ini
  • server/tests/integration/test_user_api.py
  • server/tests/integration/test_credit_api.py
  • .claude/skills/jointo-tech-stack/references/testing.md

参考

已知问题(已修复)

测试运行时出现 RuntimeError: Task got Future attached to a different loop 错误。

根本原因

  • FastAPI app 在启动时创建了自己的数据库引擎和连接池
  • 这些引擎/连接池绑定到 app 启动时的事件循环
  • 测试使用 AsyncClient 时,每个测试有自己的事件循环
  • 当 app 尝试使用旧的数据库连接时,发现事件循环不匹配

解决方案: 使用 FastAPI 的 dependency override 机制,在测试中注入测试专用的数据库会话:

from app.main import app
from app.core.database import get_session

@pytest_asyncio.fixture
async def async_client(db_session: AsyncSession) -> AsyncGenerator[AsyncClient, None]:
    """HTTP 客户端 fixture(function 级别)
    
    使用 dependency override 注入测试数据库会话,
    确保每个测试使用自己的事务并自动回滚
    """
    # Override database dependency
    async def override_get_session():
        yield db_session
    
    app.dependency_overrides[get_session] = override_get_session
    
    try:
        async with AsyncClient(app=app, base_url="http://test") as client:
            yield client
    finally:
        # Clean up
        app.dependency_overrides.clear()

关键修改

  1. engine fixture 从 session 级别改为 function 级别
  2. 每个测试使用独立的引擎,避免事件循环冲突
  3. 使用 app.dependency_overrides 注入测试数据库会话
  4. 每个测试的数据库操作在独立事务中,测试结束后自动回滚

测试结果

  • 事件循环问题已解决
  • 6/11 测试通过(包括健康检查、短信发送、登录、边界测试)
  • ⚠️ 5/11 测试失败(需要登录状态的测试,这是测试设计问题,不是框架问题)

后续工作

  • 实现 dependency override 修复事件循环问题
  • 验证基础测试能正常运行
  • 修复 api_state fixture 的作用域问题(测试间状态共享)
  • 更新测试规范文档添加 dependency override 说明