# 11 模拟测试体系、并发验证与测试沙箱要求 (Testing and Mock Plan) > **测试最高铁律**:**严禁在持续集成 (CI/CD) 或单元测试中使用真实 DMS 生产环境与真实门锁设备**! > 门锁下发密码将直接修改物理设备存储并消耗锁端电量与无线频段。 > 必须基于 **本地 Mock HTTP Server (WireMock / MockWebServer)** 搭建完备的多层级可复现测试套件。 --- ## 1. 分层测试策略体系 (Testing Strategy) ``` +--------------------------------------------------------------------------------+ | L4. 现场沙箱验收测试 (Live Sandbox E2E - 独立测试账号 + 实验台独立挂载测试锁) | +--------------------------------------------------------------------------------+ | L3. 契约与故障注入测试 (WireMock Failure Injection - 401/超时/宕机/乱序状态) | +--------------------------------------------------------------------------------+ | L2. 并发与状态机集成测试 (Concurrency & State Machine Test - 20并发Token竞争/幂等) | +--------------------------------------------------------------------------------+ | L1. 纯领域单元测试 (Unit Test - CardNumberCodec 编码 / 状态机转换 / DTO 转换) | +--------------------------------------------------------------------------------+ ``` --- ## 2. Mock DMS 仿真覆盖矩阵 (22 项核心场景) 在后续 R.3B 实现阶段,集成测试套件必须基于 Mock 服务器完整覆盖以下 22 项场景: | # | 测试场景用例 | Mock 桩行为定义 | 预期系统断言与表现 | | :-: | :--- | :--- | :--- | | 1 | **Login Success** | 返回 200,包含新 Token 与 `expiresIn: 7200` | `DmsTokenManager` 缓存 Token,设置正确到期时间戳 | | 2 | **Token Cache Reuse** | 连续 10 次请求同一接口 | 仅在第 1 次触发 Mock 登录,后续 9 次请求 Header 均复用相同 Token | | 3 | **Token Expiry** | 模拟系统时钟快进至失效时间前 5 分钟 | 自动触发平滑预防性刷新,获取新 Token | | 4 | **Concurrent Token Refresh** | **20 个并发业务线程**同时探测到 Token 缺失/过期 | **单飞排他锁生效**:Mock 登录接口**被且仅被调用 1 次**,20 个线程全部安全复用该 Token,无踩踏覆盖 | | 5 | **401 Auto Relogin** | 业务请求收到 Mock 返回 `401 Unauthorized` | 自动使缓存失效,触发单飞重登录,并**自动安全重试原请求 1 次且成功** | | 6 | **Room Lookup Normal** | 精确匹配返回单个房间 `placeId: 1320` | 正常持久化映射记录 | | 7 | **Room Lookup Not Found** | 返回 `{"success": false, "message": "Room not found!"}` | 抛出 `RoomNotFoundException`,不阻塞其他业务 | | 8 | **Room Multiple Results** | 模糊查询返回同名房间(多楼层同房号) | 触发精准过滤,若仍有多条抛出歧义异常提醒管理员人工介入 | | 9 | **Lock Lookup Normal** | 返回房间挂载的 1 把锁 | 绑定该锁并派发授权 | | 10 | **Lock Lookup Multiple** | 返回房间挂载的 2 把锁(外门+内门) | **一房多锁验证**:为该住宿安排分别生成 2 条授权记录并同时下发 | | 11 | **Lock Lookup None** | 返回空数组(该房间未装门锁) | 抛出 `LockNotConfiguredException`,状态标记为无锁,不影响入住 | | 12 | **Password Issue Accepted** | 调用 create 返回 200 + `operationId` | 本地授权状态置为 `ISSUING`,操作记录置为 `ACCEPTED`,**不得直接置为 ACTIVE** | | 13 | **Password Operation Success** | 10 秒后轮询返回 `operationStatus: "success"` | 授权状态正式推进为 `ACTIVE`,操作记录置为 `SUCCESS` | | 14 | **Password Operation Undone** | 首次轮询返回 `operationStatus: "undone"` | 保持 `ISSUING` 状态,触发第 2 次退避轮询 | | 15 | **Password Operation Failed** | 轮询返回 `operationStatus: "failed"` | 授权状态置为 `ISSUE_FAILED`,记录详细错误原因 | | 16 | **Password Operation Timeout** | 轮询返回 `operationStatus: "timeout"` | 轮询上限后停止,状态置为 `ISSUE_FAILED (TIMEOUT)`,页面显示重试按钮 | | 17 | **Card Issue & Codec** | 下发 10 进制卡号 `3161082660` | 验证 Mock 实际收到的 Body 中 `cardNumber` 为小端序 `244B6ABC` | | 18 | **Revoke Flow** | 调房或退宿时调用 delete 接口 | 正确携带 `external_key_id`,轮询成功后状态置为 `REVOKED` | | 19 | **DMS Unavailable** | Mock 服务断开或返回 HTTP 500/502 | **住宿业务事务正常提交完成**,门禁状态置为 `ISSUE_FAILED`,记录故障日志 | | 20 | **Unknown Result Reconcile** | 发送 create 时遭遇网络读取超时 | 触发未决对账流程,查询当前房间钥匙列表,避免重复发码 | | 21 | **Idempotent Duplicate Grant** | 连续快速点击 5 次“下发权限” | 本地幂等生效,仅派发 1 次 DMS 请求,无重复钥匙生成 | | 22 | **Accommodation Transfer** | 学员换房(旧房 A -> 新房 B) | 验证“先发新房 B,确认成功后再撤销旧房 A”的时序与失败补偿 | --- ## 3. 关键专项验证测试设计细节 ### 3.1 Token 并发竞争专项测试设计 (`Token Concurrency Test`) ```java // 测试伪代码设计示意 (后续 R.3B 落地至 src/test/java) @Test public void testConcurrentLoginDeduplication() throws Exception { int threadCount = 20; CountDownLatch startLatch = new CountDownLatch(1); CountDownLatch doneLatch = new CountDownLatch(threadCount); ExecutorService executor = Executors.newFixedThreadPool(threadCount); // 清空当前 Token 缓存 dmsTokenManager.invalidate(); for (int i = 0; i < threadCount; i++) { executor.submit(() -> { try { startLatch.await(); // 确保 20 个线程在瞬间同时并发冲击 String token = dmsTokenManager.getToken(); assertThat(token).isNotBlank(); } finally { doneLatch.countDown(); } }); } startLatch.countDown(); // 发令枪响 doneLatch.await(10, TimeUnit.SECONDS); // 严苛断言:Mock 服务端的登录接口在整个并发过程中被调用的次数必须精确为 1! wireMockServer.verify(1, postRequestedFor(urlEqualTo("/public/rest/third-party-app/token/get"))); } ``` ### 3.2 异步状态生命周期防抢跑断言 (`Async Anti-Premature Test`) - **断言目标**:杜绝将 HTTP 200 直接视为下发成功。 - **验证步骤**: 1. 调用 `doorAccessGateway.grantAccess(...)`。 2. 立即检查数据库中的 `train_dorm_auth` 记录: - 断言 `auth_status == ISSUING`(下发中)。 - 断言绝不能等于 `ACTIVE`。 3. 执行一次调度轮询(此时 Mock 返回 `operationStatus = undone`): - 断言 `auth_status` 依然维持在 `ISSUING`。 4. 执行第二次调度轮询(此时 Mock 返回 `operationStatus = success`): - 断言 `auth_status` 顺利跃迁为 `ACTIVE`。 --- ## 4. 生产环境接入前联调沙箱要求 (`DMS_SANDBOX_REQUIREMENTS`) 在未来正式将 RailwaySystem 部署到沈阳房建公寓段并对接真实门锁前,**必须向 DMS 供应商及公寓设备管理方书面申请独立测试沙箱环境**。 ### 4.1 核心沙箱准入条件 1. **独立测试租户**:必须提供专属的测试用 `appid`、`appsecret` 及 `projectId`,严禁与正在运行的既有公寓楼共用租户。 2. **物理实验台测试锁具**: - 必须在实验台/机房单独架设 **至少 1 台测试网关 + 2 把独立挂载的测试智能门锁**。 - 严禁直接使用学员已经实际入住的宿舍门锁进行研发调试! 3. **测试数据隔离**: - 在 DMS 中建立虚拟楼栋(如“联调测试楼”),配置测试房间(如“9901室”、“9902室”)。 4. **技术支撑配合**: - 供应商提供联调期间的技术接口人,并确认正式环境网络是否具备固定公网 IP 白名单或专网通道。