深入 Play 29:同一用例在单元、路由与真实 HTTP 中证明什么
金额 1、100 被接受,0、101 被拒绝。这组输入分别通过纯 Java 策略、Helpers.route 和独立进程 HTTP。三个层次都有价值,但相同的结果并不让它们拥有相同覆盖:纯函数没有解析 JSON,路由测试没有经过真实 TCP,网络测试也没有自动变成数据库事务测试。
分层验收需要把每条成功结论限定在它真正经过的边界。一次测试通过,只能覆盖已经执行并断言的行为。
从纯业务规则开始固定输入
ObjectPolicy.amountAllowed 是有限业务规则:amount 大于零且不超过 100。它不依赖请求、Cookie、线程或数据库。纯单元层使用相同四个输入,直接断言 boolean,失败时可以先定位规则或期望,而不必同时排查路由、Content-Type 和服务器启动。
HTTP Controller 还要处理 JSON 形状:字段存在、是整数、能转换为 int,之后才调用金额规则。业务函数能够接受 int,并不证明任意 JSON 数值都能安全进入这个范围。因此应用路由层和 HTTP 层需要补充非法 JSON、错误类型和缺字段等边界;本批实际执行的 parser 对照是截断 JSON {,不能把未执行的输入列为已覆盖。
| 测试层 | 相同输入 1、100、0、101 | 附加边界 | 当前证据 |
|---|---|---|---|
| 纯 Java | true、true、false、false | 策略与合成修改计数 | GovernanceTest.purePolicy |
| Helpers.route | 200、200、400、400 | 应用注入、router、parser、观测 Filter | GovernanceTest.routeUsesParserAndFilter |
| 独立进程 HTTP | 200、200、400、400,重复并发 | 打包配置、监听、协议、客户端读取 | governance_checks.py observations |
| 真实数据库 | 合成订单的幂等、回滚与迟提交对照 | 真实连接、事务、独立查询和资源关闭 | 累计 DatabaseLabTest 与数据库 HTTP/SQL 证据,未把金额策略替身算作数据库测试 |
本篇固定 Play 3.0.6、JDK 21,源码提交为 2e56aff7d4e7a74af61e4bd39ec9e3ed7f300cd6。历史隔离 clean test 共 37 项 JUnit,通过数由六份 XML 相加得到:PlayLabTest 7、PipelineTest 5、ContentTest 7、BudgetTest 5、AsyncTest 10、GovernanceTest 3。这个数量属于该隔离基线,不等于后续共享工程的累计测试总数。
Helpers.route 经过请求处理器
Scala Helpers.route(app, rh, body) 调用 app.requestHandler.handlerForRequest(rh),得到 tagged RequestHeader 与 Handler 后再调用执行器。测试请求 body 通过 Writeable 序列化,再进入 Action 的解析路径。因此“Helpers 一律跳过 parser 和 filter”不符合这里的实现。
与此不同,Java Helpers.routeAndCall 直接调用给定 router.route,再 invokeHandler。两者都可能执行选中的 Action 与 parser,但直接选择 router 不等于经过应用 HttpRequestHandler 的外层组合。讨论覆盖范围必须写出调用的重载和构建的应用配置。
1 | |
本批路由测试在 GuiceApplicationBuilder 中显式只启用 ObservationFilter。它验证了这个 Filter 与 parser 的组合,没有宣称该测试同时验证 CSRF、CORS 和 Host。安全矩阵由第27篇的独立专项配置进程执行,不能因为都叫“应用测试”就合并证据。
下面是可放入累计工程 test 目录的完整测试类。它复用实际 ObjectPolicy、ObservationFilter 和路由,在消费响应 body 后查询事件。该类在 JDK 21 与累计依赖下编译;完整批次的运行记录来自 GovernanceTest,避免把新增示例的编译次数计入历史 37 项。
1 | |
RequestBuilder 的调用顺序也会改变输入。早期写法先设置 Content-Type,再 bodyText,最终得到 415;将 bodyText 放前、最后明确 JSON Content-Type 后,截断 JSON 才得到预期的 parser 400。415 并非 400 的等价证据,它说明请求还没有进入计划要测的 JSON 解析分支。
手工使用 .session("username", "alice") 则是在测试请求结构中提供 session,适合验证认证 Action 与对象策略的组合,不能证明原始 Cookie 字节验签。真实网络脚本另外保存签发 Cookie、更改签名字节,再观察 401,才覆盖那一段路径。
独立进程增加配置与传输边界
脚本先运行 staged 应用,再由标准库 HTTP 和原始 WebSocket 客户端连接临时 loopback 端口。它没有借用测试 JVM 内部的 Result 对象。请求包含真实 Cookie、Origin、Host、正文和协议帧,响应由客户端实际读取。
这个边界暴露过一个应用构建问题:独立安全配置没有 include 早期 application.conf,累计 router 在构造旧异步 Controller 时找不到 lab-blocking-dispatcher,首次启动探测持续失败。补齐明确的有界 dispatcher 后,专项进程才可服务。单独的纯函数测试无法覆盖这条累计依赖关系。
真实网络层还覆盖了无法用断言 status 代替的生命周期:SSE 在原连接上的权限撤销、WebSocket 下一条消息再授权、客户端 TCP 提前关闭、响应 504 后有限后台工作完成。取消场景实际只读取 16384 字节,源端记录生产 49152 字节后终止;状态码早已是 200,成功地取得状态码不能判定 body 已收全。
测试客户端也需要资源边界。该脚本限制 HTTP/socket 超时、启动探测期限、线程池工作数、消息数量、读取大小与进程退出等待;finally 关闭原连接、停止本地代理,终止自建服务进程。最终三个进程都记录 exit 143,这是脚本有意终止的结果,不是应用自然完成全部可能任务的证明。
数据库证据不能由内存替身补齐
本治理批次的 ObjectPolicy 只有两个合成用户、两个对象和修改计数,没有 JDBC 连接。对非法访问计数不变的断言,只证明该内存分支未增加计数,不能推出事务回滚、隔离级别、连接归还或实际表中没有新行。
真实数据库验收需要保存建表与清理条件、执行 SQL、独立连接的最终查询结果、连接池终态和服务器日志,并把它们归属于运行过的数据库实验。共享接入后,专用 PostgreSQL17.6 启用时49个不同JUnit方法全部执行,包括七个DatabaseLabTest;原37项隔离测试仍不包含数据库测试,governance_checks.py也没有执行订单事务。
数据库原始 HTTP/SQL 终态见 evidence/batch22-25/shared-http-final/:同键并发只留一个订单,改变payload冲突,故意异常后订单0/库存1;HTTP504后任务未结束,独立连接分别在响应时看到0或1行,释放后都看到1行,幂等重试新增0行。evidence/batch26-29/shared-junit/TEST-DatabaseLabTest.xml 是本次接入后重新执行的数据库方法;shared-build.json记录49项执行数,网络与SQL证据没有被JUnit或截图替代。
增加数据库层时,最低对照是“同一业务输入经实际事务执行,再用独立连接核对持久状态”。只 mock 一个 repository 返回成功,或者在当前事务里查询刚写入的数据,都不足以证明提交后其他连接可见。测试说明应保留事务隔离与查询连接身份,避免扩大结果的含义。
从证据文件反查一次验收
累计工程入口见第00篇。设置 JDK 21 的 JAVA_HOME 后,在 play-lab 执行:
1 | |
历史入口为 evidence/batch26-29/isolated/test-stage-final3.log、junit/*.xml 和 run-final3/observations.json。HTTP 结果在 observations.concurrent,日志关联在 logCorrelation,计时任务容量在 boundedTimerWork,进程与字面量扫描在 processes。原始服务器日志与 JSON 同目录;source-receipt.json 保存源码文件和读取范围。
源码阅读与执行是两种不同证据。源码能解释 Helpers 选择哪个入口、JWT 异常如何返回空 session;本地测试能证明当前输入在固定版本中得到了某个结果。阅读上游测试代码没有被计入这次 JUnit 数量,页面生成或截图也没有被用来替代 socket 运行。
改动练习:保持四个金额输入不变,分别删除路由测试中的 Filter 配置、把 Content-Type 改为 text/plain、改动纯金额上界。预先写明每个变更应影响哪些层,再运行定位;不要一次修改三处后只看总测试数是否下降。
另一个练习把网络客户端改为只取响应头就关闭。断言 200 后仍等待源端终态与生产计数,比较它与完整读取消费的记录。这个对照能直接检验测试是否把响应头成功扩大为传输完成。
| 待证明的结论 | 最小相关执行层 | 不足的替代物 |
|---|---|---|
| int 金额规则正确 | 纯业务单元 | 只有页面展示 |
| 请求体确实被 parser 拒绝 | 应用请求链 | 直接调用 Controller 方法 |
| 原始 Cookie 验签有效 | 有效签发与篡改的网络请求 | 手工注入 session Map |
| 流因真实断连终止 | 实际 socket + 服务端终态 | 单看 Result.status |
| 事务已提交 | 真实数据库 + 独立查询 | 内存修改计数或 mock 返回值 |
上一篇:可观测性与完成边界。下一篇为第30篇生产启动与配置,将检查开发模式、阶段产物和独立运行进程的差异。

