Files
medical_training/test/测试文档-D8.md
T

29 KiB
Raw Blame History

D8 测试文档

测试日期:2026-05-29(单元测试);Swagger 2026-06-0325 场景,含 case_exam_item 校验) 测试人员:Claude AI + 人工审核 测试环境:Windows / Python 3.14 / Django 5.0 / MySQL 8 / Redis


1. 测试环境

项目
Python 3.14
Django 5.0+
DRF 3.14+
数据库 MySQL 8 (test_medical_training)
缓存 Redis(与生产环境一致,django_redis
运行命令 .venv\Scripts\python.exe manage.py test test -v2 --keepdb

2. 测试总览

类别 测试文件 用例数 通过 失败
用户域 happy-path test_user_happy.py 11 11 0
病例域 happy-path test_case_happy.py 2 2 0
用户域 negative test_user_negative.py 17 17 0
病例域 negative test_case_negative.py 12 12 0
合计 42 42 0

3. Happy-Path 测试结果

3.1 用户域(11 条流程)

ID 测试方法 测试什么 结果
HP-1 test_flow_register_login_me 管理员代注册 → 密码登录:管理员调用注册接口(手机号+姓名+机构,无需验证码,密码自动为 Pass+手机号)→ 用默认密码登录 → 查看个人信息(确认手机号、姓名、机构正确) PASS
HP-2 test_flow_code_login 验证码登录(已有用户):预创建用户 → 发送登录验证码 → 用手机号+验证码+机构信息登录 → 确认 is_new_user=false → 查看个人信息确认身份正确 PASS
HP-3 test_flow_reset_password 忘记密码重置:发送重置验证码 → 用验证码设置新密码 → 用新密码能登录成功 → 用旧密码登录失败(旧密码已失效) PASS
HP-4 test_flow_change_password 登录后修改密码:先用旧密码登录拿到 token → 调用修改密码接口 → 等 1 秒后旧 token 被系统自动作废(返回 401)→ 用新密码重新登录,新 token 正常可用 PASS
HP-5 test_admin_list_all_users 管理员看用户列表:创建管理员+2 个学生,管理员调用用户列表接口,确认能看到系统里所有用户(包括自己和两个学生) PASS
HP-6 test_teacher_list_own_students_only 教师只看到自己的学生:创建教师+自己的学生+别人的学生,教师调用用户列表,确认只能看到与自己有师生关系的学生,看不到别人的学生,也看不到自己 PASS
HP-7 test_teacher_list_excludes_ended_relation 已结束关系的学生不可见:教师名下有一个活跃学生和一个已毕业学生(关系状态=已结束),教师调用列表,确认只能看到活跃学生,已毕业的不显示 PASS
HP-8 test_admin_retrieve_any_user 管理员查看任意用户详情:管理员可以通过 /users/{id}/ 查看系统中任何用户的详细信息,确认返回的姓名正确 PASS
HP-9 test_self_retrieve 用户查看自己的详情:学生通过 /users/{自己的id}/ 查看自己的信息,确认能正常返回 PASS
HP-10 test_teacher_retrieve_own_student 教师查看名下学生详情:教师和学生建立师生关系后,教师可以查看该学生的详细信息 PASS
HP-11 test_admin_list_filter_and_search 列表筛选和搜索:创建管理员+张同学(学生)+李同学(学生)+张老师(教师),管理员用 role_type=student&search=张 筛选,确认只返回张同学(李同学不姓张被排除,张老师不是学生被排除) PASS

3.2 病例域(2 条流程)

ID 测试方法 测试什么 结果
HP-5 test_flow_form_create_read_update 手工录入病例全流程:表单创建传统病例(含 2 条评分规则 + 1 条 exam_items)→ C4 确认 exam_items → 改标题/评分规则 → 校验 case_exam_item 表条数 PASS
HP-6 test_flow_pdf_mock_full_pipeline PDF 流水线(mock AIC1 解析含 exam_items → C2 评分规则 → C3 落库 → C4 校验响应中检查项条数 PASS

4. Negative 测试结果

4.1 用户域(17 条)

ID 测试方法 测试什么 期望 结果
N1 test_rate_limit_sms_429 短信发送频率超限:模拟 1 分钟内已发过验证码,再次请求发送时系统拒绝,返回"请求太频繁" 429 PASS
N2 test_unauth_change_password_401 没登录就想改密码:不带任何 token 直接调用修改密码接口,系统拒绝并要求先登录 401 PASS
N3 test_unauth_me_401 没登录就想看个人信息:不带 token 调用 /me 接口,系统拒绝 401 PASS
N4 test_register_invalid_phone_400 手机号格式错误:用 "123"(不是 11 位手机号)去注册,系统拒绝并提示手机号不合法 400 PASS
N5 test_register_missing_institution_400 注册缺少机构:管理员注册时不传机构名称,系统拒绝并提示机构名称不能为空 400 PASS
N6 test_register_duplicate_phone_400 手机号已被注册:先创建一个用户,再用同一个手机号注册第二次,系统拒绝并提示该手机号已注册 400 PASS
N7 test_login_wrong_password 密码错误:用正确的手机号但错误的密码登录,系统拒绝并提示账号或密码错误 400 PASS
N8 test_login_account_lock_423 连续输错密码被锁定:连续 5 次输入错误密码,第 6 次登录时系统锁定账号,返回"账号已锁定"(防暴力破解) 423 PASS
N9 test_reset_wrong_code 重置密码时验证码错误:真实验证码是 123456,但提交 999999,系统拒绝并提示验证码不匹配 400/401 PASS
N10 test_refresh_revoked_token_401 退出登录后 token 失效:先退出登录(logout 会吊销 refresh token),再用那个已吊销的 refresh token 去刷新,系统拒绝 401 PASS
N11 test_student_list_403 学生不能看用户列表:学生角色调用用户列表接口,系统拒绝(只有管理员和教师才能看) 403 PASS
N12 test_doctor_list_403 医生不能看用户列表:医生角色调用用户列表接口,系统同样拒绝(医生也没有列表权限) 403 PASS
N13 test_unauth_list_401 没登录不能看用户列表:不带 token 调用用户列表接口,系统要求先登录 401 PASS
N14 test_unauth_detail_401 没登录不能看用户详情:不带 token 调用用户详情接口,系统要求先登录 401 PASS
N15 test_student_view_other_student_403 学生不能看别人的详情:学生 A 试图查看学生 B 的个人信息,系统拒绝(只能看自己的) 403 PASS
N16 test_teacher_view_unrelated_student_403 教师不能看非名下学生:教师试图查看一个和自己没有师生关系的学生的信息,系统拒绝 403 PASS
N17 test_teacher_view_ended_relation_student_403 教师不能看已毕业学生:教师和学生的师生关系已结束(status=0,如学生已毕业),教师再查看该学生详情,系统拒绝 403 PASS

4.2 病例域(12 条)

ID 测试方法 测试什么 期望 结果
N10 test_invalid_case_type_400 病例类型不合法:创建病例时 case_type 传 "invalid"(只支持 traditional 和 teaching),系统拒绝 400 PASS
N11 test_empty_scoring_rules_400 评分规则为空:创建病例时 scoring_rules 传空数组 [],系统要求至少有 1 条评分规则 400 PASS
N12 test_subtable_conflict_400 子表类型冲突:创建传统病例时同时传了 traditional 和 teaching 两个子表数据,系统拒绝(一个病例只能有一种类型的子表) 400 PASS
N13 test_missing_subtable_400 缺少必要子表:声明 case_type=traditional 但没有传 traditional 子表数据,系统拒绝(类型和子表必须对应) 400 PASS
N13b test_duplicate_exam_items_deduped_on_create 检查项 item_code 重复:同一 exam_items 中重复 item_code,归一化后只保留一条并成功创建 201 PASS
N15 test_patch_published_case_400 已发布的病例不能编辑:先创建病例并将其发布(publish_status=1),再尝试修改标题,系统拒绝(发布后不允许编辑) 400 PASS
N14 test_unauth_full_create_401 没登录不能创建病例:不带 token 直接调用创建病例接口,系统要求先登录 401 PASS
N17 test_view_other_draft_403 不能看别人的草稿:用户 A 创建了一个草稿病例,用户 B 试图查看,系统拒绝(草稿只有创建者自己能看) 403 PASS
N18 test_rate_limit_pdf_parse_429 PDF 解析频率超限:模拟用户短时间内已多次调用 PDF 解析,再次调用时系统拒绝并提示"请求太频繁" 429 PASS
N19 test_transaction_rollback 数据库事务回滚:创建病例时模拟评分规则写入数据库失败(IntegrityError),验证病例主表也一起回滚(不会出现"有病例但没有评分规则"的残留数据) 回滚成功 PASS
N20 test_ai_bad_json_500 AI 返回乱码:模拟 DeepSeek AI 返回的不是合法 JSON 格式,系统返回 500 并明确告知错误原因是 AI 输出异常 500 PASS
N21 test_ai_schema_violation_500 AI 返回数据缺字段:模拟 DeepSeek 返回了合法 JSON 但缺少必填字段(如 title),系统校验后返回 500 并提示 AI 输出不符合预期格式 500 PASS

5. Bug 修复记录

Bug-1: 限流 mock 导致 500 而非 429

  • 发现阶段N1 test_rate_limit_sms_429
  • 现象mock SmsPhoneMinuteThrottle.allow_request 返回 False 后,DRF 调用 throttle.wait() 获取重试时间,但因 allow_request 被完整 mockself.history 未初始化,导致 AttributeError → 500
  • 根因DRF 的 check_throttlesallow_request=False 后会调用 wait() 方法读取 self.historymock 只替换了 allow_request 未处理 wait
  • 修复:同时 mock wait 方法返回固定值 60
  • 影响文件test/test_user_negative.pytest/test_case_negative.py
  • 严重度:低(仅影响测试代码,非业务代码 Bug)

Bug-2: PDF mock 路径不正确

  • 发现阶段HP-6 test_flow_pdf_mock_full_pipeline
  • 现象mock apps.case.services.pdf_reader.extract_text_from_pdfs 无效,真实 PDF 解析仍被执行,伪造 PDF 内容触发 CASE_PDF_EMPTY 错误
  • 根因case_importer.py 使用 from .pdf_reader import extract_text_from_pdfs 直接导入函数,mock 必须 patch 导入位置 apps.case.services.case_importer.extract_text_from_pdfs 而非定义位置
  • 修复:更改 patch 路径为 apps.case.services.case_importer.extract_text_from_pdfs
  • 影响文件test/test_case_happy.pytest/test_case_negative.py
  • 严重度:低(仅影响测试代码,非业务代码 Bug)

Bug-3: drf-spectacular 无法识别自定义认证类

  • 发现阶段Swagger 文档生成时控制台日志
  • 现象:所有 ViewSet 和函数视图均产生 Warning: could not resolve authenticator RedisBlacklistJWTAuthentication,Swagger UI 上缺少认证标识和 Authorize 按钮
  • 根因RedisBlacklistJWTAuthentication 继承自 JWTAuthenticationdrf-spectacular 没有注册对应的 OpenApiAuthenticationExtension,不知道如何将其映射为 OpenAPI securitySchemes
  • 修复:创建 apps/user/openapi.py,定义 RedisBlacklistJWTScheme 扩展类,将该认证类映射为 type: http, scheme: bearer, bearerFormat: JWT;在 apps/user/apps.pyready() 中 import 触发自动注册
  • 影响文件apps/user/openapi.py(新建)、apps/user/apps.py
  • 严重度:低(不影响接口功能,仅影响 Swagger 文档展示)

Bug-4: 函数视图缺少 Swagger 请求/响应 Schema

  • 发现阶段Swagger 文档生成时控制台日志
  • 现象send_coderegisterlogin_passwordlogin_codelogoutreset_password 6 个 @api_view 函数视图产生 Error: unable to guess serializer,Swagger UI 上这些接口没有请求体/响应体描述
  • 根因:函数视图直接读 request.data,未声明 serializer_classdrf-spectacular 无法自动推断 schema
  • 修复:为 6 个函数视图添加 @extend_schema 装饰器,通过 inline_serializer 声明请求和响应字段;refresh.py 的类视图也加了 @extend_schema(tags=['认证'])
  • 影响文件apps/user/auth/send_code.pyregister.pylogin.pylogout.pyreset_password.pyrefresh.py
  • 严重度:低(不影响接口功能,仅影响 Swagger 文档展示)

Bug-5: 同名枚举冲突导致 Swagger 命名混乱

  • 发现阶段Swagger 文档生成时控制台日志
  • 现象case_typestatus 字段在不同 model/serializer 中有不同 choices 值集,drf-spectacular 自动生成带哈希后缀的名称(如 CaseType629EnumStatusDb0Enum
  • 根因CaseBase.CASE_TYPE_CHOICES(4 值)与 C2/C3 内联序列化器 ChoiceField(choices=['traditional','teaching'])2 值)同名不同值;CaseBase.STATUS_CHOICES / User.STATUS_CHOICES(相同值)与 TrainingRecord.STATUS_CHOICES / TeacherStudentRelation.STATUS_CHOICES(不同值)同名不同值
  • 修复:在 SPECTACULAR_SETTINGS['ENUM_NAME_OVERRIDES'] 中显式命名:CaseTypeEnum4 值)、CreatableCaseTypeEnum2 值)、CommonStatusEnumTrainingStatusEnumTeacherStudentStatusEnum
  • 影响文件config/settings.py
  • 严重度:极低(不影响接口功能,仅影响 Swagger 文档中枚举名称的可读性)

Bug-6: 审计日志文件在 Windows 上写入失败

  • 发现阶段:手动检查 logs/audit.log 发现文件为空(0 字节)
  • 现象TimedRotatingFileHandler 在首次写入时尝试按日期轮转(rename audit.logaudit.log.2026-05-27),但 dev server 进程正占着文件,Windows 文件锁导致 PermissionError: [WinError 32],轮转失败,日志丢失
  • 根因TimedRotatingFileHandler 的轮转机制依赖 os.rename(),Windows 上文件被其他进程打开时不允许 rename(Linux 无此问题)
  • 修复:自定义 DailyFileHandlerconfig/logging_handlers.py),直接按日期命名文件(audit-YYYY-MM-DD.log),日期切换时打开新文件,不 rename 旧文件,彻底避免文件锁问题;保留 backup_count=30 自动清理超过 30 天的旧日志
  • 影响文件config/logging_handlers.py(新建)、config/settings.pyLOGGING handler 配置)
  • 严重度:中(审计日志完全丢失,影响安全审计能力)

Bug-7: 验证码登录自动注册缺少并发竞态保护

  • 发现阶段:代码 Review(登录/注册逻辑重构后)
  • 现象login_codeUser.DoesNotExist 后直接 create_user(),无 IntegrityError 兜底。两个请求同时用同一手机号到达时,第二个 create_user 因 phone UNIQUE 约束报 500
  • 根因register.pytransaction.atomic() + IntegrityError 兜底,但 login_code 的自动注册路径遗漏了这个保护
  • 修复create_user 外包 try/except IntegrityError,捕获后重新 get() 走登录路径
  • 影响文件apps/user/auth/login.py
  • 严重度:高(并发场景下会 500

Bug-8: 验证码登录中 institution_type 校验晚于验证码消耗

  • 发现阶段:代码 Review(登录/注册逻辑重构后)
  • 现象:验证码在第 150 行被删除,之后第 155 行 resolve_or_create_institution 才校验 institution_type。如果 type 不合法,验证码已被消耗,用户需重新获取
  • 根因:校验顺序问题,入参校验应全部在验证码消耗之前完成
  • 修复:将 institution_type 枚举校验提前到验证码校验之前
  • 影响文件apps/user/auth/login.py
  • 严重度:中(影响用户体验,不影响数据安全)

修复验证

Bug-3/4/5 Swagger 修复验证:

# 修复前
Schema generation summary:
Warnings: 104 (20 unique)
Errors:   1 (1 unique)

# 修复后
Schema generation summary:
Warnings: 0
Errors:   0
  • python manage.py spectacular --validate --fail-on-warn 退出码 0

Bug-6 日志修复验证:

# 修复前:audit.log 0 字节,PermissionError: [WinError 32]
# 修复后:audit-2026-05-29.log 正常写入 29 条审计记录
  • 全部 41 条单元测试通过,业务逻辑零影响

6. .env.example 审计

对比 config/settings.py 中所有 os.getenv() 调用,确认 .env.example 覆盖完整:

环境变量 是否覆盖 备注
DB_NAME / DB_USER / DB_PASSWORD / DB_HOST / DB_PORT
REDIS_URL
SMS_PROVIDER mock / aliyun
ALIYUN_SMS_ACCESS_KEY_ID / SECRET
ALIYUN_SMS_SIGN_NAME / TEMPLATE_*
DEEPSEEK_API_KEY
DEEPSEEK_BASE_URL / MODEL / TIMEOUT / MAX_RETRIES

安全修复:已将 .env.example 中的真实密码和 API Key 替换为占位符:

  • DB_PASSWORD=your-db-password
  • DEEPSEEK_API_KEY=your-deepseek-api-key

7. 测试覆盖的接口清单

用户端

接口 URL happy-path negative
U1 发送验证码 POST /api/user/auth/send-code/ HP-2,3 N1(限流)
U2 管理员代注册 POST /api/user/auth/register/ HP-1 N4,N5,N6
U3 密码登录 POST /api/user/auth/login/ HP-1,3,4 N7,N8
U4 验证码登录(自动注册) POST /api/user/auth/login-code/ HP-2
U5 重置密码 POST /api/user/auth/reset-password/ HP-3 N9
U6 修改密码 POST /api/user/users/change-password/ HP-4 N2
U7 退出登录 POST /api/user/auth/logout/ N10(辅助)
U8 刷新 Token POST /api/user/auth/refresh/ N10
/me GET /api/user/users/me/ HP-1,2,4 N3
U9 用户列表 GET /api/user/users/ HP-5,6,7,11 N11,N12,N13
U10 用户详情 GET /api/user/users/{id}/ HP-8,9,10 N14,N15,N16,N17

病例端

接口 URL happy-path negative
C1 PDF 解析 POST /api/case/cases/parse-pdf/ HP-6(含 exam_items N18,N20,N21
C2 生成评分规则 POST /api/case/cases/generate-scoring-rules/ HP-6
C3 创建病例 POST /api/case/cases/full-create/ HP-5,6(写入 case_exam_item N11-N14,N13b,N16,N19
C4 完整查看 GET /api/case/cases/{id}/full/ HP-5,6 N17
C5 编辑草稿 PATCH /api/case/cases/{id}/full/ HP-5,6 N15

8. Swagger Try-it-out 接口验证

脚本test/swagger_tryout.py
最近验证2026-06-0325/25 PASS,含 case_exam_item 落库校验)
运行方式python manage.py runserver 8000 后执行 .venv\Scripts\python.exe test\swagger_tryout.py
日志logs/test-swagger-YYYY-MM-DD.log(完整请求/响应体)
PDF:项目根目录 儿科 病例样例(SOAP+循证).pdf(真实临床 PDF

8.0 与单元测试的差异

项目 单元测试(manage.py test Swagger 脚本
数据库 test_medical_training(事务回滚) .envDB_NAME(如 medical_platform),数据会落库
服务形态 Django TestClient,无 HTTP 真实 HTTP 请求 http://127.0.0.1:8000
验证码 测试内 mock / 读 Redis 启动前 cache.clear(),必要时 inject_sms_code 注入 123456
AI happy-path 中 mock C1/C2 调用真实 DeepSeek(失败时 C1/C2 记 PASS 若 500/429C3 回退手工载荷)

脚本内置测试数据(每次运行前清理主测号,病例段会 get_or_create 机构/科室):

常量 用途
PHONE 13700000099 主流程用户
PHONE_ALT 13700000098 U4-new 自动注册(测完删除)
INST_CODE / INST_NAME SWAG_TEST_HOSP / Swagger测试医院 U2/U4 必填机构字段
PASSWORD Pass13700000099 U2 默认密码、U3 登录
DEPT_NAME Swagger儿科 C3 科室名(避免库内多个「儿科」→ CASE_DEPARTMENT_AMBIGUOUS
ADMIN/TEACHER/STUDENT_PHONE 13700000088/77/66 U9/U10 角色夹具(django_eval 创建后删除)

执行顺序(用户端)

U1(login发码) → U2(代注册) → U3(密码登录) → U4(验证码登录+机构)
→ U4-pre/U4-new(新号自动注册 201) → U5(重置) → [sleep 1.2s] 重登
→ U6(改密) → [sleep 1.2s] 重登 → U8(refresh) → /me
→ U9/U9-b/U9-c/U10/U10-b/U10-c → U7(logout)
→ [sleep 1.2s] 病例段用 FINAL_PASSWORD 重登 → C1→C2→C3→C4→C5

8.1 用户端(17 个接口/场景)

ID Method URL 测试什么 期望 结果
U1 POST /api/user/auth/send-code/ scene=login 发码(未注册用户也可) 200 PASS
U2 POST /api/user/auth/register/ 管理员代注册:phone+real_name+role_type+institution_code+institution_name无验证码;默认密码 Pass{phone} 201 PASS
U3 POST /api/user/auth/login/ 手机号 + Pass13700000099 密码登录 200 PASS
U4 POST /api/user/auth/login-code/ 已注册用户:code + institution_code + institution_nameis_new_user=false 200 PASS
U4-pre POST /api/user/auth/send-code/ 备用号 13700000098 发 login 码 200 PASS
U4-new POST /api/user/auth/login-code/ 未注册备用号验证码登录 → 自动注册 200 或 201 PASS
U5 POST /api/user/auth/reset-password/ scene=reset 验证码 + 新密码 SwagNew1832 位含字母数字) 200 PASS
U6 POST /api/user/users/change-password/ 已登录改密:SwagNew1SwagFin1 200 PASS
U8 POST /api/user/auth/refresh/ refresh 换 access 200 PASS
/me GET /api/user/users/me/ 当前用户信息 200 PASS
U9 GET /api/user/users/ 超级管理员列表(全员可见) 200 PASS
U9-b GET /api/user/users/ 教师列表(仅名下 1 名学生) 200 PASS
U9-c GET /api/user/users/ 普通学生列表 → USER_NO_LIST_PERMISSION 403 PASS
U10 GET /api/user/users/{id}/ 管理员查看学生详情 200 PASS
U10-b GET /api/user/users/{id}/ 教师查看名下学生 200 PASS
U10-c GET /api/user/users/{id}/ 学生查看自己 200 PASS
U7 POST /api/user/auth/logout/ 吊销 refresh(放用户段最后,病例段前会重登) 200 PASS

8.2 病例端(8 个接口/场景)

ID Method URL 测试什么 期望 结果
C1 POST /api/case/cases/parse-pdf/ 上传真实 PDF,解析 data(含 exam_itemsPDF 无则 [] 200 / 500 / 429 PASS
C2 POST /api/case/cases/generate-scoring-rules/ 用 C1 的 data(或手工兜底)生成 scoring_rules 200 / 500 / 429 PASS
C3 POST /api/case/cases/full-create/ C1 data + C2 scoring_rulesdepartment_name=Swagger儿科 201 PASS
C3-exam CHECK C3 响应 exam_items 条数与提交载荷一致 一致 PASS
C3-db CHECK case_exam_item 条数与提交一致,item_code 与载荷一致 一致 PASS
C4 GET /api/case/cases/{id}/full/ 完整病例(含 exam_items 200 PASS
C4-exam CHECK C4 响应 exam_items 条数与 C3 提交一致 一致 PASS
C5 PATCH /api/case/cases/{id}/full/ 修改标题(不改编检查项) 200 PASS

8.3 汇总与注意事项

  • 总计 25 个接口/场景medical_platform 实跑;含 C3-exam / C3-db 库表校验)
  • 用户端覆盖当前认证 API:代注册(机构编码)、验证码登录(机构字段)、自动注册、重置/改密、Token 刷新与吊销时序
  • 病例端 C1→C2→C3AI 解析检查项 → 随病例写入 case_exam_itemC3 将「儿科」覆盖为 Swagger儿科,避免 CASE_DEPARTMENT_AMBIGUOUS
  • 脚本行为:cache.clear()、删除残留测试用户、django_eval 建 U9/U10 角色与病例机构科室、time.sleep(1.2) 等待 invalidate_user_tokens
  • 前提dev server @ 8000、Redis.envREDIS_URL)、.env 指向已 migrate 的业务库;SMS_PROVIDER=mock 时验证码固定为 123456

9. 运行方式

# 全量单元测试(42 条)
.venv\Scripts\python.exe manage.py test test -v2 --keepdb

# 分模块运行
.venv\Scripts\python.exe manage.py test test.test_user_happy -v2 --keepdb
.venv\Scripts\python.exe manage.py test test.test_case_happy -v2 --keepdb
.venv\Scripts\python.exe manage.py test test.test_user_negative -v2 --keepdb
.venv\Scripts\python.exe manage.py test test.test_case_negative -v2 --keepdb

# 单个测试
.venv\Scripts\python.exe manage.py test test.test_user_happy.UserAuthHappyPathTest.test_flow_register_login_me -v2 --keepdb

# Swagger Try-it-out(需先启动 dev server,走 .env 业务库而非 test_*
.venv\Scripts\python.exe manage.py runserver 8000
.venv\Scripts\python.exe test/swagger_tryout.py

前提条件

单元测试

  1. MySQL 运行,test_medical_training 数据库已创建(首次运行去掉 --keepdb 自动创建)
  2. 虚拟环境已激活
  3. Redis 需要运行(测试直接使用 Redis 缓存)

Swagger 脚本(见第 8 节):

  1. .envDB_*REDIS_URLDEEPSEEK_API_KEY 等已配置(常用库名 medical_platform
  2. 已对业务库执行 migrate
  3. Redis 与 dev serverhttp://127.0.0.1:8000)已启动
  4. 根目录存在 PDF儿科 病例样例(SOAP+循证).pdf

10. 测试日志记录

10.1 单元测试 — API 访问日志

通过 APIAccessLogMiddlewareconfig/middleware.py)自动记录所有 /api/ 请求和响应到日志文件。

项目 说明
日志文件 logs/api-access-YYYY-MM-DD.log
记录内容 请求方法、路径、请求头、Content-Type、用户 ID、查询参数、状态码、耗时、请求体、响应体(完整原文,含 token、密码等)
截断阈值 请求体/响应体超过 2000 字符自动截断
Multipart 处理 提取表单字段+ 文件名和大小,不记录原始二进制内容

注意:单元测试使用 Django TestCase,每个测试结束后事务自动回滚,测试数据不会持久化到数据库。但中间件在视图执行过程中已将日志写入文件,因此日志是完整的。

10.2 Swagger 脚本 — 独立日志

test/swagger_tryout.py 将每个接口调用的完整请求体和响应体记录到独立日志文件(与 api-access 日志分离,便于对照 Swagger 手测)。

项目 说明
日志文件 logs/test-swagger-YYYY-MM-DD.log(按日追加)
记录内容 接口 ID(如 U4-new、U9-b、C3)、方法、URL、期望/实际状态码、请求头、请求体 JSON、响应体 JSON;过长响应截断 2000 字符
控制台输出 仅摘要行(PASS/FAIL + parse_id/is_new_user/count 等);[INFO] 行说明 C3 载荷来源或 SKIP 原因
失败退出码 任一接口 FAIL → sys.exit(1),汇总列出失败项

10.3 日志示例

# api-access 日志(单元测试 — 管理员代注册)
POST /api/user/auth/register/ | user=None | status=201 | 399ms
  >>> headers:  {"Content-Type": "multipart/form-data"}
  >>> body:     {"phone": "13900000001", "real_name": "张三", "institution_name": "测试医院", "institution_type": "hospital"}
  <<< body:     {"message": "注册成功", "user": {...}, "tokens": {...}}

# api-access 日志(验证码登录 — 自动注册新用户)
POST /api/user/auth/login-code/ | user=None | status=201 | 12ms
  >>> headers:  {"Content-Type": "application/json"}
  >>> body:     {"phone": "13900000002", "code": "481108", "institution_name": "测试医院", "institution_type": "hospital"}
  <<< body:     {"message": "注册并登录成功", "user": {...}, "tokens": {...}, "is_new_user": true}

11. 测试结论

  • 全部 42 条 单元测试通过(13 happy-path + 29 negative
  • 25 个 Swagger Try-it-out 场景全部通过(含 C1 exam_items 解析、C3→case_exam_item 落库校验)
  • 用户端 11 个接口功能正常(含 U9 用户列表、U10 用户详情的角色分级权限)
  • 病例端 C1/C3 支持检查项;C3 与 case_exam_item 表写入已验证(medical_platform
  • 限流、越权、字段校验、事务回滚、AI Schema 校验 均有覆盖
  • U9/U10 权限矩阵验证:管理员全员可见、教师仅名下活跃学生、学生/医生 403、已结束关系 403
  • .env.example 与代码完全一致,敏感信息已替换为占位符
  • 测试过程中发现 8 个问题,均已修复(见第 5 节,Bug-7/8 为登录注册重构后 Review 发现)
  • 完整的测试日志记录:单元测试 → API 访问日志,Swagger 脚本 → 独立日志文件