# 测试规范修正:移除硬编码 BASE_URL **日期**: 2026-01-28 **类型**: 测试规范优化 **影响范围**: 测试框架 ## 背景 在测试代码中发现硬编码的 `BASE_URL = "http://localhost:6170/api/v1"`,这会导致: - 测试依赖特定端口号 - 无法适配不同测试环境 - 违反了使用 AsyncClient 的最佳实践 ## 修正内容 ### 1. 移除硬编码 BASE_URL **修改前**: ```python BASE_URL = "http://localhost:6170/api/v1" # 硬编码 response = await client.get(f"{BASE_URL}/users/me") ``` **修改后**: ```python @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: ```python @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): ```python BASE_URL = "http://localhost:6170/api/v1" # 硬编码,依赖端口 response = await client.get(f"{BASE_URL}/users/me") ``` ✅ **正确写法**(使用 fixture 提供 API 前缀): ```python @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: ```ini # 标记(markers) markers = integration: 集成测试(需要真实数据库、Redis) unit: 单元测试(使用 Mock) slow: 慢速测试 # 注意:asyncio 由 pytest-asyncio 插件提供,不要重复声明 ``` ### 6. 修正 conftest.py 的 db_session fixture 使用正确的事务回滚写法: **修改前**: ```python 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() ``` **修改后**: ```python 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` ## 参考 - [pytest fixtures 文档](https://docs.pytest.org/en/stable/fixture.html) - [httpx AsyncClient 文档](https://www.python-httpx.org/async/) - [FastAPI 测试文档](https://fastapi.tiangolo.com/tutorial/testing/) ## 已知问题(已修复) 测试运行时出现 `RuntimeError: Task got Future attached to a different loop` 错误。 **根本原因**: - FastAPI app 在启动时创建了自己的数据库引擎和连接池 - 这些引擎/连接池绑定到 app 启动时的事件循环 - 测试使用 AsyncClient 时,每个测试有自己的事件循环 - 当 app 尝试使用旧的数据库连接时,发现事件循环不匹配 **解决方案**: 使用 FastAPI 的 dependency override 机制,在测试中注入测试专用的数据库会话: ```python 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 测试失败(需要登录状态的测试,这是测试设计问题,不是框架问题) ## 后续工作 - [x] 实现 dependency override 修复事件循环问题 - [x] 验证基础测试能正常运行 - [ ] 修复 `api_state` fixture 的作用域问题(测试间状态共享) - [ ] 更新测试规范文档添加 dependency override 说明