ReconnectPolicyTest(11 项)验的是**纯函数**「给定关闭码应该怎么退避」,而 ⑦
「连接频繁重连」的缺陷往往出在**调用点**。新增 PoseWebSocketReconnectTest(8 项):
用 MockWebServer.withWebSocketUpgrade 在 JVM 上起**真实** WebSocket 服务端
(能按 §3.2 回 welcome、按 §3.6 回 pong、按指定关闭码关连接),无需设备即可覆盖
「该重连的」与「不该重连的」两条分支。Robolectric 只用来提供 android.os.Build 桩。
它抓到的真实缺陷(本版修复):
上一次会话的关闭码会污染下一次会话,导致重新开始推流后失去自动重连能力。
复现:第一次连接被对端以 1001 关闭(按 §7 不重连,正确)→ 用户再点「开始推流」
→ 这一次是网络层失败(服务端还没起来),按 §7 **应当**重连 → 但 connect() 只重置了
reconnectCounter 与 lastPongAtMs,**没有清 lastCloseCode** → scheduleReconnect()
仍按 1001 判定 → 直接 stopped,**再也不会自动重连**。
测试失败信息(修复前):
✗ 上一次 1001 关闭后重新连接,遇到网络失败仍应按策略重连
AssertionError: 当前 state=STOPPED,lastCloseCode=1001,attempts=0
修复(PoseWebSocketClient.kt):
* connect():清掉会话级关闭状态(lastCloseCode=0、lastCloseReason="");
* onFailure():记录 recordClose(0, …)(契约 §7 中 0=未收到关闭帧 → 应重连),
顺带让界面「最近一次断开原因」显示本次失败而不是旧值。
用例清单:握手→streaming 与 welcome 保活参数解析、按协商间隔发 ping 收 pong、
1011 按退避重连且等待落在策略 ±20% 区间、成功后计数清零、1001/1002 不重连、
主动断开不重连、上一次 1001 后再连遇网络失败仍应重连(本次回归锁)、
服务端未启动时 1.5s 内 ≤3 次尝试(退避生效)。
实测:./gradlew :app:testDebugUnitTest → 6 个测试类 / 64 项用例 / 0 失败
(原 56 项);build/verify.sh 全绿(签名、APK 版本元数据 0.2.1/201、native 库体检、
ABI、R8、dex)。版本号 0.2.0/200 → 0.2.1/201。
踩坑:第一版 MockWebServer 夹具的 101 响应没有回显 Sec-WebSocket-Protocol
(RFC 6455 要求),客户端记录的 negotiatedProtocol 为空导致断言失败 —— 那是
**夹具**不合规而非客户端问题,已在夹具里补上。写「真实对端」夹具时要按 RFC/契约
把服务端做对,否则会把夹具缺陷误判成产品缺陷。
4.3 KiB
4.3 KiB
T-AND-016:重连链路的 JVM 集成测试(真实 WebSocket)+ 修一处真实缺陷
- 状态: done
- 依赖: T-AND-011(连接状态机与重连)
- 文档: v1.6 §6.5 / §7.6
- 契约:
websocket-keypoints 0.2.0§7 / §7.1 / §8
为什么需要
ReconnectPolicyTest(11 项)验的是纯函数「给定关闭码应该怎么退避」;
但 ⑦「连接频繁重连」的缺陷往往出在调用点:关闭码有没有被正确记录、
重连计数有没有在成功后清零、上一次的状态会不会污染下一次会话。
这些只有把真实的 OkHttp WebSocket 接到真实服务端上才能验出来。
因此新增 PoseWebSocketReconnectTest(8 项):用 MockWebServer.withWebSocketUpgrade
在 JVM 上起真实 WebSocket 服务端(能按 §3.2 回 welcome、按 §3.6 回 pong、
按指定关闭码关连接),无需设备即可覆盖两条分支:该重连的与不该重连的。
Robolectric 只用来提供 android.os.Build.* 桩。
它抓到的真实缺陷(本版修复)
上一次会话的关闭码会污染下一次会话,导致新版一启动就失去自动重连能力。
复现路径:
- 第一次连接被对端以
1001关闭 →lastCloseCode = 1001,按契约 §7 不重连(正确); - 用户再次点「开始推流」→
connect(); - 这一次是网络层失败(服务端还没起来 / 被拒),按 §7 应当重连;
- 但
connect()只重置了reconnectCounter与lastPongAtMs,没有清lastCloseCode;scheduleReconnect()仍按 1001 判定 → 直接stopped,再也不会自动重连。
测试输出的失败信息(修复前):
✗ 上一次 1001 关闭后重新连接,遇到网络失败仍应按策略重连
java.lang.AssertionError: 上一次的 1001 不应影响本次会话:网络失败后必须按 §7 重连
(当前 state=STOPPED,lastCloseCode=1001,attempts=0)
修复(PoseWebSocketClient.kt):
| 位置 | 改动 |
|---|---|
connect() |
清掉会话级关闭状态:lastCloseCode = 0、lastCloseReason = ""(每次新会话都从干净状态开始) |
onFailure() |
记录 recordClose(0, …)(契约 §7 中 0 = 未收到关闭帧 → 应重连),同时让界面「最近一次断开原因」显示本次失败而不是旧值 |
用例清单(8 项)
| 用例 | 验的是什么 |
|---|---|
握手成功 → streaming,解析 welcome 的 sessionId / 保活参数,hello 带 deviceId |
§3.1 / §3.2 发送端一侧 |
按协商间隔发送应用层 ping 并收到 pong |
§1.1 / §3.6(协议层 ping 不能替代) |
异常关闭码 1011 → 按退避重连、等待时间落在策略区间、成功后计数清零 |
§7 / §7.1 / §8 |
1001 对端离开 → stopped 且不重连,日志记录「不重连」 |
§7 |
1002 协议错误 → 不重连 |
§7 |
用户主动 disconnect() → 不重连 |
§7 |
| 上一次 1001 后再连、遇网络失败仍应重连 | 本次修复的缺陷(回归锁) |
| 服务端未启动时的连续尝试遵守退避(1.5 s 内 ≤3 次) | §7.1「不会疯狂重连」 |
实测
$ ./gradlew :app:testDebugUnitTest
BUILD SUCCESSFUL
测试类 6 个,用例 64 项,失败 0,跳过 0
(新增 8 项;修复前该项失败,其余 7 项通过)
build/verify.sh 全绿(含签名、版本元数据、native 库体检、ABI、R8、dex)。
踩坑记录
夹具也要合规:第一版 MockWebServer 的 101 响应没有回显
Sec-WebSocket-Protocol(RFC 6455 要求服务端带上选中的子协议),于是客户端记录的
negotiatedProtocol 是空串,断言失败。那是夹具不合规,不是客户端问题 ——
已在夹具里补上 setHeader("Sec-WebSocket-Protocol", …)。
写这类"真实对端"夹具时,要按 RFC / 契约把服务端做对,否则会把夹具缺陷误判成产品缺陷。
未覆盖(诚实声明)
- 真机 Wi-Fi 抖动、切网、进程被杀、Doze 省电等真实断网场景仍需设备实测;
- 契约 §7.1 的封顶 15 s 行为由纯函数单测覆盖(穷举 1..8 次), 集成测试只验证「等待时间取自策略且落在 ±20% 区间」,否则单测要跑 30 秒以上。