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
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()
优势
- 环境无关:不依赖特定端口号
- 统一管理:API 前缀集中在 fixture 中
- 易于维护:修改 API 版本只需改一处
- 符合最佳实践:使用 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.pyserver/tests/pytest.iniserver/tests/integration/test_user_api.pyserver/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()
关键修改:
- 将
enginefixture 从 session 级别改为 function 级别 - 每个测试使用独立的引擎,避免事件循环冲突
- 使用
app.dependency_overrides注入测试数据库会话 - 每个测试的数据库操作在独立事务中,测试结束后自动回滚
测试结果:
- ✅ 事件循环问题已解决
- ✅ 6/11 测试通过(包括健康检查、短信发送、登录、边界测试)
- ⚠️ 5/11 测试失败(需要登录状态的测试,这是测试设计问题,不是框架问题)
后续工作
- 实现 dependency override 修复事件循环问题
- 验证基础测试能正常运行
- 修复
api_statefixture 的作用域问题(测试间状态共享) - 更新测试规范文档添加 dependency override 说明