23 KiB
23 KiB
单元测试
**本文档引用的文件** - [tests/conftest.py](file://tests/conftest.py) - [tests/test_auth.py](file://tests/test_auth.py) - [tests/test_citation_engine.py](file://tests/test_citation_engine.py) - [tests/test_citations.py](file://tests/test_citations.py) - [tests/test_queries.py](file://tests/test_queries.py) - [backend/app/main.py](file://backend/app/main.py) - [backend/app/api/deps.py](file://backend/app/api/deps.py) - [backend/app/api/auth.py](file://backend/app/api/auth.py) - [backend/app/api/citations.py](file://backend/app/api/citations.py) - [backend/app/api/queries.py](file://backend/app/api/queries.py) - [backend/app/workers/citation_engine.py](file://backend/app/workers/citation_engine.py) - [backend/app/services/auth.py](file://backend/app/services/auth.py) - [backend/app/models/user.py](file://backend/app/models/user.py) - [backend/app/models/citation_record.py](file://backend/app/models/citation_record.py)目录
简介
本文件系统性梳理 GEO 项目的单元测试实现,覆盖认证模块、引用引擎、查询处理与引用数据四大模块的测试用例设计与最佳实践。文档重点说明测试夹具(fixture)的使用方法(如 mock_user、auth_token、auth_headers、override_get_current_user),断言编写方式与覆盖率建议,并提供异步函数、依赖注入与错误处理的测试示例路径与流程图。
项目结构
测试目录位于仓库根目录 tests/,采用按功能分模块组织的方式,分别对应后端 API 层与核心业务组件:
- 认证模块测试:tests/test_auth.py
- 引用引擎测试:tests/test_citation_engine.py
- 查询处理测试:tests/test_queries.py
- 引用数据测试:tests/test_citations.py
- 测试夹具与异步客户端:tests/conftest.py
- 后端应用入口与路由:backend/app/main.py、backend/app/api/*.py
- 核心业务组件:backend/app/workers/citation_engine.py
- 服务层与模型:backend/app/services/、backend/app/models/
graph TB
subgraph "测试层"
T1["tests/test_auth.py"]
T2["tests/test_citation_engine.py"]
T3["tests/test_queries.py"]
T4["tests/test_citations.py"]
C["tests/conftest.py"]
end
subgraph "后端应用"
M["backend/app/main.py"]
A1["backend/app/api/auth.py"]
A2["backend/app/api/queries.py"]
A3["backend/app/api/citations.py"]
D["backend/app/api/deps.py"]
S1["backend/app/services/auth.py"]
W["backend/app/workers/citation_engine.py"]
U["backend/app/models/user.py"]
CR["backend/app/models/citation_record.py"]
end
T1 --> A1
T2 --> W
T3 --> A2
T4 --> A3
T1 --> D
T2 --> W
T3 --> D
T4 --> D
A1 --> S1
A2 --> D
A3 --> D
D --> U
W --> CR
M --> A1
M --> A2
M --> A3
C --> M
图表来源
- tests/conftest.py:1-71
- backend/app/main.py:1-48
- backend/app/api/auth.py:1-43
- backend/app/api/queries.py:1-86
- backend/app/api/citations.py:1-78
- backend/app/api/deps.py:1-43
- backend/app/services/auth.py:1-69
- backend/app/workers/citation_engine.py:1-309
- backend/app/models/user.py:1-41
- backend/app/models/citation_record.py:1-42
章节来源
核心组件
本节概述各模块测试关注点与测试夹具的作用范围。
-
认证模块测试(tests/test_auth.py)
- 注册成功/重复邮箱、登录成功/密码错误、用户信息获取(含未认证场景)
- 使用夹具:mock_registered_user、async_client、patch 注入服务层行为
- 关键断言:HTTP 状态码、响应体字段、错误详情消息
-
引用引擎测试(tests/test_citation_engine.py)
- 品牌匹配器精确/别名/模糊/无匹配;竞争品牌检测;引用位置与上下文提取
- 使用夹具:直接构造类实例进行纯函数式断言,无需外部依赖
-
查询处理测试(tests/test_queries.py)
- 创建查询成功/超出限额、列出查询、更新查询、删除查询、查询不存在/不属于他人
- 使用夹具:mock_query、async_client、patch 服务层返回值或异常
-
引用数据测试(tests/test_citations.py)
- 获取引用列表、统计信息、CSV 导出(MIME 类型、附件头、内容片段)
-
测试夹具(tests/conftest.py)
- mock_scheduler:屏蔽后台调度器,避免真实任务影响测试
- mock_user:模拟认证用户对象,包含 id、邮箱、计划、配额等
- override_get_current_user:重写依赖 get_current_user,使路由自动获得认证用户
- auth_token:基于 mock_user 生成 JWT
- auth_headers:组装 Authorization Bearer 头
- async_client:ASGI 异步 HTTP 客户端,用于端到端测试
章节来源
- tests/test_auth.py:1-104
- tests/test_citation_engine.py:1-54
- tests/test_queries.py:1-154
- tests/test_citations.py:1-93
- tests/conftest.py:19-71
架构总览
下图展示测试夹具与被测组件之间的交互关系,以及异步 HTTP 客户端如何驱动 FastAPI 路由与依赖注入。
sequenceDiagram
participant Test as "测试用例"
participant Fixture as "测试夹具(conftest.py)"
participant Client as "AsyncClient"
participant App as "FastAPI 应用(main.py)"
participant Dep as "依赖注入(deps.py)"
participant Service as "服务层(API 路由)"
participant Model as "模型(models)"
Test->>Fixture : 请求夹具(mock_user/override_get_current_user/...)
Fixture-->>Test : 返回夹具对象
Test->>Client : 发起 HTTP 请求
Client->>App : ASGI 请求
App->>Dep : 解析 OAuth2 Bearer Token
Dep-->>App : 返回当前用户(get_current_user)
App->>Service : 调用路由处理器
Service->>Model : 数据库读写/查询
Service-->>App : 返回响应数据
App-->>Client : HTTP 响应
Client-->>Test : 断言结果
图表来源
- tests/conftest.py:28-71
- backend/app/main.py:38-42
- backend/app/api/deps.py:16-43
- backend/app/api/auth.py:13-43
- backend/app/api/queries.py:15-86
- backend/app/api/citations.py:25-78
详细组件分析
认证模块测试
- 测试要点
- 注册接口:成功返回 201,响应包含邮箱与姓名;重复邮箱抛出 400
- 登录接口:成功返回 200,包含 access_token 与用户信息;错误凭据返回 401
- 用户信息接口:已认证返回 200,未认证返回 401
- 测试夹具
- mock_registered_user:模拟注册成功的用户对象
- override_get_current_user:通过依赖覆盖提供认证用户
- auth_headers:携带 Bearer Token 的请求头
- async_client:ASGI 异步客户端
- 断言策略
- 状态码断言:201/200/400/401
- 响应体字段断言:access_token、token_type、user.email 等
- 错误详情断言:detail 中包含特定提示
- 异步与依赖注入示例路径
sequenceDiagram
participant T as "测试用例(test_auth.py)"
participant F as "夹具(conftest.py)"
participant AC as "AsyncClient"
participant R as "路由(auth.py)"
participant S as "服务(auth.py)"
participant DB as "数据库"
T->>F : 获取 mock_registered_user/override_get_current_user/auth_headers
T->>AC : POST /api/v1/auth/register
AC->>R : 路由处理器
R->>S : register_user(...)
S->>DB : 查询/插入用户
S-->>R : 返回用户对象
R-->>AC : 201 + 用户信息
AC-->>T : 断言状态码与字段
图表来源
- tests/test_auth.py:25-40
- tests/conftest.py:28-62
- backend/app/api/auth.py:13-19
- backend/app/services/auth.py:37-52
章节来源
- tests/test_auth.py:1-104
- tests/conftest.py:28-62
- backend/app/api/auth.py:13-43
- backend/app/services/auth.py:37-69
引用引擎测试
- 测试要点
- BrandMatcher:精确匹配置信度 1.0、别名匹配置信度 0.9、模糊匹配阈值 >0.4、无匹配返回 False/None/0.0
- CompetitorDetector:在文本中识别除目标品牌外的竞争品牌
- 引用位置与上下文:按段落定位首次出现位置并截取上下文片段
- 测试夹具
- 直接构造 BrandMatcher/CompetitorDetector 实例,无需外部依赖
- 断言策略
- 字典键断言:cited、match_type、confidence、position、citation_text
- 列表断言:competitor_brands 包含预期品牌且不含目标品牌
- 示例路径
classDiagram
class BrandMatcher {
+match(text) dict
-_extract_candidates(text) list
-_extract_position_and_context(text, keyword) tuple
}
class CompetitorDetector {
+detect(text, target_brand) list
}
BrandMatcher --> CompetitorDetector : "在引擎中协作"
图表来源
章节来源
查询处理测试
- 测试要点
- 创建查询:成功返回 201,响应包含关键词与目标品牌;超出配额抛出 403
- 列出查询:返回 items 与 total
- 更新查询:返回更新后的关键词与频率
- 删除查询:返回 204
- 查询不存在/不属于他人:返回 404
- 测试夹具
- mock_query:模拟查询对象,包含 id、keyword、target_brand、platforms、frequency、status 等
- override_get_current_user、auth_headers、async_client
- 断言策略
- 状态码断言:201/200/204/403/404
- 响应体字段断言:keyword、frequency、detail 等
- 示例路径
sequenceDiagram
participant T as "测试用例(test_queries.py)"
participant F as "夹具(conftest.py)"
participant AC as "AsyncClient"
participant R as "路由(queries.py)"
participant S as "服务层"
participant DB as "数据库"
T->>F : 获取 mock_query/override_get_current_user/auth_headers
T->>AC : POST /api/v1/queries/
AC->>R : 路由处理器
R->>S : create_query(...)
S->>DB : 插入查询记录
S-->>R : 返回查询对象
R-->>AC : 201 + 查询信息
AC-->>T : 断言状态码与字段
图表来源
章节来源
引用数据测试
- 测试要点
- 获取引用列表:返回 items 与 total,包含 platform 与 cited 等字段
- 统计信息:返回总量、引用率、按平台分布与趋势
- CSV 导出:返回 text/csv 内容类型与附件头,内容包含平台信息
- 测试夹具
- mock_citation_record:模拟引用记录对象
- override_get_current_user、auth_headers、async_client
- 断言策略
- 状态码断言:200
- 响应体断言:total、items、by_platform、trend 等
- 响应头断言:Content-Type 以 text/csv 开头、Content-Disposition 含 attachment
- 示例路径
sequenceDiagram
participant T as "测试用例(test_citations.py)"
participant F as "夹具(conftest.py)"
participant AC as "AsyncClient"
participant R as "路由(citations.py)"
participant S as "服务层"
participant DB as "数据库"
T->>F : 获取 mock_citation_record/override_get_current_user/auth_headers
T->>AC : GET /api/v1/citations/stats
AC->>R : 路由处理器
R->>S : get_citation_stats(...)
S->>DB : 聚合统计
S-->>R : 返回统计结果
R-->>AC : 200 + 统计信息
AC-->>T : 断言字段与头
图表来源
章节来源
测试夹具详解与最佳实践
- mock_scheduler
- 作用:屏蔽后台调度器,避免真实任务启动影响测试稳定性
- 使用:在会话级 autouse fixture 中替换 app.main.query_scheduler 的 start/shutdown
- 参考路径:tests/conftest.py:19-26
- mock_user
- 作用:提供认证用户对象,包含 id、email、name、plan、max_queries、is_active 等
- 使用:作为 override_get_current_user 的数据源
- 参考路径:tests/conftest.py:29-39
- override_get_current_user
- 作用:重写依赖 get_current_user,使路由自动解析到 mock_user
- 使用:在测试函数参数中注入,结束后清理依赖覆盖
- 参考路径:tests/conftest.py:42-50
- auth_token
- 作用:基于 mock_user 的 id 生成 JWT
- 使用:配合 auth_headers 进行认证请求
- 参考路径:tests/conftest.py:54-56
- auth_headers
- 作用:组装 Authorization: Bearer 请求头
- 使用:GET/POST/PUT/DELETE 等请求统一携带
- 参考路径:tests/conftest.py:60-62
- async_client
- 作用:ASGI 异步 HTTP 客户端,用于端到端测试
- 使用:with 上下文管理器确保生命周期内正确创建与释放
- 参考路径:tests/conftest.py:65-71
最佳实践
- 尽量使用 patch 替换服务层或外部依赖,避免真实网络或数据库调用
- 对于需要认证的路由,优先使用 override_get_current_user 与 auth_headers
- 对于后台任务,使用 mock_scheduler 屏蔽真实调度器
- 对于异步函数,使用 pytest.mark.asyncio 并通过 AsyncClient 发起请求
- 清理:在依赖覆盖后及时 pop,避免影响其他测试
章节来源
异步函数、依赖注入与错误处理测试示例
- 异步函数
- 使用 pytest.mark.asyncio 标记测试函数
- 使用 ASGI AsyncClient 发起请求
- 参考路径:tests/test_auth.py:25-40、tests/test_queries.py:30-48
- 依赖注入
- 通过 app.dependency_overrides 临时替换 get_current_user
- 参考路径:tests/conftest.py:42-50、backend/app/api/deps.py:16-43
- 错误处理
- 通过 patch 抛出异常或返回 None,验证路由的 HTTP 异常与错误详情
- 参考路径:tests/test_auth.py:43-58、tests/test_queries.py:51-71
章节来源
- tests/test_auth.py:25-58
- tests/test_queries.py:51-71
- tests/conftest.py:42-50
- backend/app/api/deps.py:16-43
测试数据准备与清理
- 准备
- 使用 AsyncMock 构造模型对象(用户、查询、引用记录),设置必要字段
- 使用 patch 模拟服务层返回值或异常
- 使用 override_get_current_user 提供认证上下文
- 清理
- 测试结束后 pop 依赖覆盖,避免污染其他测试
- 使用 with 上下文管理 async_client 生命周期
- 参考路径
章节来源
- tests/conftest.py:42-50
- tests/conftest.py:65-71
- tests/test_citations.py:8-21
- tests/test_queries.py:10-27
依赖分析
- 测试对应用层的依赖
- 测试通过 ASGI AsyncClient 直接调用 FastAPI 路由,路由再依赖 get_current_user 解析 JWT
- get_current_user 依赖 OAuth2PasswordBearer 与 verify_token,最终查询数据库 User 表
- 测试对服务层的依赖
- 通过 patch 替换服务层函数(如 register_user、authenticate_user、create_query 等)
- 测试对业务组件的依赖
- 引擎测试直接构造 BrandMatcher/CompetitorDetector,不依赖外部服务
graph LR
T["测试用例"] --> C["conftest.py 夹具"]
C --> A["FastAPI 路由"]
A --> D["依赖注入(get_current_user)"]
D --> S["服务层"]
S --> DB["数据库"]
T --> E["引用引擎(直接类实例)"]
图表来源
- tests/conftest.py:28-71
- backend/app/api/deps.py:16-43
- backend/app/api/auth.py:13-43
- backend/app/api/queries.py:26-39
- backend/app/workers/citation_engine.py:19-146
章节来源
- tests/conftest.py:28-71
- backend/app/api/deps.py:16-43
- backend/app/api/auth.py:13-43
- backend/app/api/queries.py:26-39
- backend/app/workers/citation_engine.py:19-146
性能考虑
- 使用 ASGI AsyncClient 避免真实网络往返,提升测试速度
- 通过 patch 替换外部服务调用,减少 IO 与等待时间
- 使用 mock_scheduler 屏蔽后台任务,避免并发与定时任务干扰
- 对纯函数(引擎类)直接实例化断言,避免数据库与网络依赖
故障排查指南
- 401 未认证
- 检查是否正确注入 override_get_current_user 与 auth_headers
- 参考路径:tests/test_auth.py:88-104、tests/conftest.py:42-62
- 403 超出配额
- 检查服务层是否抛出 PermissionError,测试中通过 patch 触发
- 参考路径:tests/test_queries.py:51-71
- 404 不存在/不属于他人
- 检查 get_query 返回 None 的分支,确认依赖覆盖与参数
- 参考路径:tests/test_queries.py:128-139、tests/test_queries.py:143-154
- CSV 导出问题
- 检查 Content-Type 与 Content-Disposition 头,以及响应体是否包含平台信息
- 参考路径:tests/test_citations.py:76-93
章节来源
- tests/test_auth.py:88-104
- tests/test_queries.py:51-71
- tests/test_queries.py:128-154
- tests/test_citations.py:76-93
- tests/conftest.py:42-62
结论
本测试体系通过夹具与 patch 有效隔离外部依赖,结合 ASGI 异步客户端完成端到端验证。认证、查询、引用数据三大模块均覆盖成功与错误路径,引擎模块以纯函数形式保证可测试性与高内聚。建议持续补充边界条件与并发场景测试,逐步提升整体覆盖率。
附录
- 测试覆盖率建议
- 路由层:100%(含错误分支)
- 服务层:100%(含异常路径)
- 引擎类:100%(精确/别名/模糊/无匹配、位置与上下文)
- 数据模型:100%(字段与索引覆盖)
- 参考文件