09.11 应用测试与交付:把 IM 接口合同变成可复核证据
返回第九卷 · HTTP 合同:09.02 · 测试基础:10.02 · CI 前置:10.09 · 安全前置:09.08
09.11 应用测试与交付:把 IM 接口合同变成可复核证据
DeepTutor 初稿经技术与教学审阅后的静态课程。测试代码、运行结果、报告与故障均为纸上方案;本次没有执行 Go 测试、数据库、IM 服务、CI 或站点。学习者日后在自己的隔离环境执行时,须把真实输入、输出和环境另行保存,不能把本文的预期写成“已通过”。
一、先定“当前合同”,再挑测试工具
要检验虚构教学 IM,第一步不是打开 go test,而是读清 09.02 的当前 S2 HTTP 合同:POST /v1/conversations/c-a/messages 接收 JSON,正文最多 6 个 UTF-8 字节;"你好" 恰好 6 字节,可在本进程内存受理并返回 200、status=accepted_in_memory;"你好呀" 为 9 字节,应拒绝且内存列表不变;同会话同 message_id 再提交返回 409,不覆盖原消息。原始 HTTP 请求体的上限是 4096 B,与 6 B 正文上限不同。
工程卷 10.09 的 R9 是一项待评审的变更练习:拟将正文上限从 6 B 提到 9 B。R9 的 "你好呀" 会变合法,10 字节应拒绝;它的预期不能混入 09.02 当前合同的绿色报告。若将来真正批准 R9,要同时更新需求、API 文档、测试预期、客户端提示和发布/兼容记录。06/09 卷其余当前应用例子仍以 6 B 为基线。
| 版本/情景 | "你好" 6 B | "你好呀" 9 B | "中文甲a" 10 B | 可宣称的状态 |
|---|---|---|---|---|
| 09.02 当前 S2 合同 | 允许 | 拒绝 | 拒绝 | 仅内存受理,非持久或设备送达 |
| R9 待审变更 | 允许 | 拟允许 | 拟拒绝 | 只有变更获批并在目标实现验证后才生效 |
测试的真值来源是需求与业务合同,而不是现有函数恰好返回什么。对每个情景应先写:可信主体、会话、输入与单位、预期 HTTP 状态/机器码、状态变化、可见性和未覆盖结果。用一个快照把当前实现输出复制为“预期”,容易让错误实现和测试一起变绿。
二、四层证据各验证到哪一段
10.02 已教纯规则和表驱动测试。应用层要再把证据按实际边界分层,避免“单元测试通过”被解读为“网络、数据库和设备都正确”:
| 层次 | 本章可设计的检查 | 能证明的范围 | 不能单独证明 |
|---|---|---|---|
| 纯规则/应用单元 | 6 B 边界、重复 ID 决策、错误身份、拒绝后状态 | 给定输入和可控依赖时的规则 | HTTP 序列化、真实数据库、客户端收到回应 |
httptest handler | 方法、JSON、状态/头、稳定错误码、认证主体传递 | 被调用的 handler/middleware 在测试内的表现 | 真实 TCP/TLS、数据库约束、浏览器 Cookie 行为 |
| 隔离数据库集成 | 参数 SQL、唯一约束、事务、迁移旧行、资源关闭 | 指定驱动/数据库版本与种子数据下的行为 | 浏览器、消息队列、设备送达与生产负载 |
| 端到端/客户端 | 真实协议、身份凭据、网络回复和所选设备观察 | 明确部署与观察点下的完整路径 | 未测试的故障、所有设备与长期可靠性 |
一个 fake 存储可控制“超时”“重复冲突”,检查应用会不会错误映射为成功;它不能证明 PostgreSQL 真的执行了该唯一约束,或 MongoDB Cursor 真的被正确关闭。反过来,隔离数据库的 INSERT 成功也不能证明浏览器 A 收到了 200。每张报告都要在标题上写清层次、版本、输入和确认点。
三、Go httptest:从请求、响应到状态断言
net/http/httptest 提供测试用的服务端请求和响应记录器。httptest.NewRequest 构造可传给 http.Handler 的入站请求,httptest.NewRecorder 记录 handler 写出的状态、头和正文,Result() 得到可检查的 *http.Response。例如未来学习者在隔离练习中,可为 u-a 的合法 POST 准备测试认证器、内存存储和 JSON 请求,再检查:
req := httptest.NewRequest(http.MethodPost,
"/v1/conversations/c-a/messages",
strings.NewReader(`{"message_id":"m-a","body":"你好"}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
// handler 由个人练习中的私有测试装配提供:含受控认证器和存储。
handler.ServeHTTP(rec, req)
resp := rec.Result()
defer resp.Body.Close()
// 随后核对 StatusCode、Content-Type、JSON 字段与存储状态。这只是片段,不是已执行测试或完整程序。测试身份要通过明确的私有测试装配注入,不能为了方便给公开 handler 留一个“X-Test-User: u-a 即视为登录”的生产后门。断言也不能只看 StatusCode==200:还要检查 Content-Type、可解析的 JSON、message_id/status、发送者来自受信身份,以及内存里恰新增一条。拒绝用例则要检查状态未变,错误体不泄露原始 JSON、令牌或其他用户历史。
ResponseRecorder 是直接调用 handler,不经过真实网络或 TLS。若要让真实 http.Client 通过 TCP 请求测试服务端协议边界,可使用 httptest.NewServer/NewTLSServer 并在结束时关闭它;这仍是隔离测试服务器,不能因此宣称生产代理、浏览器和数据库都正确。Result().Body 也要关闭,避免示例本身教出资源泄漏习惯。
四、把当前合同写成正例、负例和状态变化表
下面是 09.02 当前 S2 合同 的最小矩阵,预期与实际结果分栏;作者只填写预期,不伪造实际输出。u-a/u-b 是 c-a 成员,u-c 已登录但不是成员;登录方案本身是 09.07 的纸上前置。
| 输入/动作 | 当前预期 | 必查的状态/数据 | 尚未覆盖 |
|---|---|---|---|
A GET 空的 c-a 历史 | 200、空列表和分页字段 | 不增删消息 | 持久性、设备状态 |
A POST m-a/"你好",正文 6 B | 200、accepted_in_memory | 内存恰新增一条,发送者 A | 客户端是否最终收到、B 是否收到 |
A POST m-a/"你好呀",正文 9 B | 按当前上限拒绝,TOO_LONG 类别 | 列表/旧正文不变 | R9 尚未生效 |
A POST 同一 m-a、不同正文 | 409 DUPLICATE_MESSAGE | 旧消息未覆盖、顺序不变 | S5 幂等重试回应 |
| 未认证请求 | 401 UNAUTHENTICATED | 不读/写受保护数据 | 登录系统的真实运行 |
已认证非成员 C GET c-a | 按 09.02 的隐藏政策 404 NOT_FOUND | 不泄露 c-a 历史或成员 | 其他策略下的 403 选择 |
| 已认证成员做禁止的管理动作 | 403 FORBIDDEN | 不改成员关系 | 管理权限完整实现 |
| 请求体实际读取到 4097 B | 413 REQUEST_TOO_LARGE | 不进入成功提交 | 正文 6 B 校验是另一层 |
| 非法 JSON/未知字段 | 400 与稳定错误类别 | 不新增消息 | 客户端兼容策略 |
09.02 的错误表原先列了“空正文/坏 JSON”等 400,却未单独给正文超 6 B一行。本轮将它补为 400 TOO_LONG,让上表有可复核的接口预期。HTTP 原始体过 4096 B 的 413 与正文业务上限的 400 不能混用。
对分页另准备三条 m-a/m-b/m-c 的纸上历史,limit=2 期望先 m-c,m-b,下一页以绑定同一会话/过滤条件的游标得到 m-a。在真实测试前先写明如何处理“响应在内存追加后丢失”:handler 的 Recorder 可检查服务端打算写什么,不能证明 A 实际收到完整响应;网络故障变式需另有客户端观察和服务端提交证据。正例、负例、边界和结果未知应一起展示,不能只收集绿色的正常请求。
五、把授权矩阵和安全负例变成回归入口
09.07 给出“主体×动作×资源×当前状态”的权限矩阵。测试也沿这个矩阵展开,而不是到处散落“某接口返回 403”的孤立样例。尤其要把攻击者改变路径里的 conversation_id 或 message_id、请求体伪造 sender_id 与旧 WebSocket 连接继续发信列为明确负例:
| 主体与动作 | 可检查的预期 | 需要哪一层真实证据 |
|---|---|---|
C 把 GET 路径改成 c-a | 拒绝且不泄露私有历史 | handler 授权测试;未来隔离服务测试 |
A 在 POST 体写 sender_id=u-b | 按 09.02 当前 DTO 拒绝未知字段;不能冒充 B | JSON/认证/状态断言 |
| A 退群后复用旧 Session/WS | 后续 c-a 动作按政策拒绝 | 授权集成与连接生命周期测试 |
| Cookie 表单缺 CSRF 证据 | 状态改变被请求来源防护拒绝 | 浏览器/HTTP 入口测试;仅有 fake 不够 |
| 含引号的会话值 | 只作为 SQL 值;格式不合法则按字段规则拒绝 | 参数化 SQL 集成与授权负例 |
| HTML 样式正文 | 普通文本安全输出 | 模板/浏览器输出测试,不只查数据库值 |
附件上传、服务端链接预览尚未在当前 S2/S3 教学服务中实现。09.08 给了防护设计和纸上负例,但不能在本章报告里勾选“附件扫描通过”“SSRF 已挡住”。若未来真正加入这两项功能,再按文件类型/容量、权限、路径、重定向/DNS 和出口边界新增相应隔离验证。OWASP 的授权测试方法强调由权限矩阵产生测试,以免新功能增加后漏掉对象级拒绝路径。
六、隔离依赖与故障:不要让 fake 充当数据库
可控 fake 很适合验证应用面对依赖错误时会作何决定:例如数据访问返回唯一冲突,应用映射为重复消息;返回超时,应用不随口承诺“肯定没写”;成员资格在写入前变化时,应用按 06.07 的业务顺序拒绝或重试。设计 fake 时,不要直接复制真实存储实现的每一行逻辑,那只会让同一个错误在两处同时存在。预期来自业务规则,fake 只控制边界条件。
隔离数据库集成验证则针对真实驱动/模式:m-a 唯一约束、(conversation_id,seq) 排序、旧消息快照 NULL、事务回滚、迁移前后字段兼容,以及 Rows.Close/Err 的可观察失败。需要临时、可重建的虚构数据;不连接真实公司数据库,不依赖某位开发者本地残留表。每项运行都应注明数据库/驱动版本、初始种子、清理方式与失败时是否有部分状态。提交阶段断网导致结果未知要有受控故障或明确标为未验证;一次静态代码审阅不能证明数据库一定没提交。
单元、handler 与数据库集成各有自己的“绿”;合并成总表时必须保留层次和缺口,不能用一个 go test ./... 成功替所有跨进程、异步队列或设备确认。OpenIM 固定源码的异步 MsgToMQ/Mongo 消费对照仍是另外一条路径,教学 SQL 集成测试不能当上游性能或持久性报告。
七、CI、配置和文档也要能让人复核
10.09 的最小 CI 设计把检查结果绑定到具体提交 SHA、Go 版本、命令、虚构数据版本和构建/报告制品。对一项 R9 待审 PR,报告还必须标“验证的是拟议 9 B 合同”,不能用来宣称当前 09.02 的 6 B 接口已改变。失败日志至少指向哪条合同、哪个样本、期望/实际和阶段;别打印 Token、真实聊天正文或数据库连接串。
交付文档至少要让另一名学习者回答:如何取得虚构测试数据、启动个人隔离环境、选择当前合同或拟议 R9、设置请求体/正文上限、获得测试凭据、运行哪类检查、怎样读取错误码和游标、迁移哪个模式版本、怎么关闭/清理资源、哪些功能仍未实现。数据库 DSN、签名密钥和 Cookie 秘密应通过受控配置说明名称与来源,不能把真实值写进课程 README。
| 证据状态 | 文档里应怎样写 | 不应写成 |
|---|---|---|
| 纸上预期 | “按 09.02 预期,9 B 正文应拒绝” | “服务已拒绝,测试通过” |
| 个人实测 | 提交、版本、命令、输入、输出、环境和时间 | 只贴一张没有来源的绿色截图 |
| 尚未验证 | “MongoDB、真实 WebSocket 断连、设备送达未覆盖” | 沉默或用单元测试的绿色代替 |
制品是一次工作流的输出,缓存只是加速;即使制品附了 SHA-256,也只说明文件内容可比,不自动证明业务、授权或安全验收已完成。交付应有可定位失败与未覆盖范围,不是堆很多没有解释的命令。
八、交付评审包与分层练习
本章的静态交付包包含:当前 6 B 与 R9 拟议版两张独立合同矩阵;一个 httptest 请求/响应示意;主体/动作/资源的正负授权表;fake 与隔离数据库的证据范围;一份错误与提交未知的故障表;一个带提交、版本、数据、命令、结果、未验证项的记录模板。学习者未来执行后,才可把某一项从“纸上预期”移到“个人实测”。
分层练习:先答,再展开反馈
1. 当前 09.02 的正文上限是多少?
6 个 UTF-8 字节,R9 的 9 B 是待审变更。
2. 当前合同下 `"你好"` 与 `"你好呀"` 分别怎样?
前者 6 B 可受理,后者 9 B 应拒绝。
3. R9 待审报告可证明当前 6 B 合同已经生效为 9 B 吗?
不能。拟议版本与已生效版本的预期和证据必须分开。
4. 测试的预期从现有函数输出抄来可靠吗?
不可靠。真值应来自独立需求、合同和状态承诺。
5. 纯函数测试通过能证明 A 收到了 HTTP 200 吗?
不能,它不覆盖网络和客户端观察。
6. `httptest.NewRecorder` 会真的经过 TCP/TLS 吗?
不会。它记录直接调用 handler 的输出。
7. `httptest.NewServer` 能证明生产代理与设备送达吗?
不能。它只在隔离测试服务器中走真实 HTTP。
8. handler 测试为什么还要检查 JSON 和存储状态?
状态码单独不能说明返回字段正确或失败后数据未被修改。
9. 为何不能把 `X-Test-User` 直接留在公开 handler 当登录后门?
客户端可伪造该头;测试身份须仅由私有测试装配控制。
10. 当前合同的重复 m-a 应有什么结果?
409 DUPLICATE_MESSAGE,旧正文与顺序不变。
11. 原始请求体 4097 B 与正文 9 B 应返回同一错误吗?
不应。前者是请求体资源上限 413,后者是正文业务上限 400 TOO_LONG。
12. u-c 已登录却请求 c-a 历史,本章预期是什么?
按当前隐藏目标政策对外 404,内部仍记录授权拒绝。
13. 客户端改 message_id 就能看别人的消息吗?
不能。每个对象仍须按已认证主体做资源授权。
14. A 在 JSON 中填 sender_id=u-b,本章 DTO 怎样处理?
09.02 当前合同拒绝这个未知字段;发件人仍由受信身份决定。
15. SQL 中含引号的 ID 一定要按“注入成功”报错吗?
不是。参数绑定让它只是值;字段格式是否合法与对象授权另判。
16. fake 存储能证明真实数据库唯一约束吗?
不能,只能证明应用面对 fake 给定错误的决策。
17. 隔离数据库 INSERT 成功能证明 B 设备收到吗?
不能,设备交付是另一确认点。
18. Commit 附近断网能直接写“消息未保存”吗?
不能。结果可能未知,需按稳定身份查验。
19. 附件与预览尚未实现,可填“安全测试通过”吗?
不能。可写纸上防护设计和将来要验证的负例。
20. 绿色 CI 报告至少要能定位哪些输入?
提交 SHA、工具链、命令、数据版本、合同版本与检查结果。
21. 缓存命中等于测试结果制品吗?
不等于。缓存是加速,报告/构建输出是本次运行制品。
22. “未验证的故障路径”应怎样写进交付文档?
明确列出路径、所需环境与下一份证据,不能拿其他层的绿色代替。
来源与下一步
- Go
testing、net/http/httptest与Go 测试教程:测试入口、请求记录器和隔离服务器。 - OWASP:授权测试矩阵与ASVS 项目:把主体/动作/对象政策转成负例和可追踪要求。
- Go:事务与GitHub:工作流制品:数据边界与运行证据身份。
按学习路线,先读04.04 可靠传输并补齐 04.04–04.07 的传输、连接、流控和 TLS,再用09.12 完整教学 IM 服务项目把身份、会话、历史、消息规则与验证清单组合起来;S5 的异步发送/重试与协议兼容仍另成章。离开本章前,应能对每份“通过”报告说明验证的是哪个合同版本、在哪层运行、真正观察到了什么、还有什么没验证。