mqtt-plus 架构解析(九):测试体系,为什么要同时有 MqttTestTemplate 和 EmbeddedBroker
摘要
很多框架在测试设计上都会掉进两个极端:要么全是纯单元测试,覆盖很快但离真实协议链太远;要么什么都拉起真 broker 来跑,虽然更接近真实场景,但速度、稳定性和定位成本都很高。mqtt-plus-test 当前提供了两种互补测试方式:MqttTestTemplate.simulateIncoming(...) 用于快速 router 级测试,@EnableMqttPlusTest 则提供 embedded MQTT broker 与测试辅助配置,用来支撑更接近真实链路的 Spring 测试。本文会结合 MqttTestTemplate、EmbeddedMqttBroker、EmbeddedMqttBrokerInitializer 和相关测试,拆解这套分层测试模型为什么成立。
项目地址
项目地址:
https://github.com/mqttplus/mqtt-plus
配套的示例工程:
https://github.com/mqttplus/mqtt-plus-examples
如果你对这个方向感兴趣,欢迎关注、试用,也欢迎一起交流 issue 和 PR。
如果这篇文章对你有帮助,欢迎点赞、收藏,也欢迎给项目一个 Star。
到了第 9 篇,系列已经基本把框架内部主链路讲完了。
但一个架构如果只讲实现,不讲测试,很容易留下一个空白:
- 这些路由、转换、恢复、自动装配,到底是怎么被验证的?
- 为什么 mqtt-plus 没有把所有测试都压成一种方式?
mqtt-plus-test到底是一个"方便写测试的小工具包",还是一套有明确分层思想的测试模型?
README 里其实已经把答案说得很直白了:
MqttTestTemplate.simulateIncoming(...)适合快速 router 级测试@EnableMqttPlusTest适合带 embedded MQTT broker 的 Spring 测试
也就是说,它从一开始就不是要你"二选一",而是明确提供两种互补路径。
一、这篇文章到底想回答什么?
这一篇只回答三个问题:
- 为什么 mqtt-plus 的测试体系要分成快测和真 broker 测两层
MqttTestTemplate和@EnableMqttPlusTest分别覆盖了哪些边界- 为什么这套测试设计比"全都拉起 broker"或者"全都 mock"更平衡
如果只记住一句话,那就是:
mqtt-plus 的测试体系不是在问"哪一种测试最好",而是在用不同成本的测试手段覆盖不同层级的问题。
二、先看这套测试体系的分层结构
先把这两种测试路径放在一张图里看清楚。
Router-level fast test
MqttTestTemplate.simulateIncoming(...)
MqttMessageRouter.route()
Listener invocation / converter / interceptor / error strategy
Embedded broker Spring test
@EnableMqttPlusTest
EmbeddedMqttBrokerInitializer
Embedded MQTT broker + Spring context
Real MQTT clients / real protocol exchange
这张图里最关键的是:
- 上面这条链是"快路径",直接切到 router
- 下面这条链是"真路径",真的起 broker、真的连客户端、真的走协议交互
也就是说,mqtt-plus 的测试分层不是围绕"目录结构"做的,而是围绕"你到底想验证哪一段链路"来设计的。
三、MqttTestTemplate 到底快在哪里?
MqttTestTemplate 的实现比名字还简单。
它本质上只做了一件事:
- 接收
brokerId、topic、payload、headers - 然后直接调用
messageRouter.route(...)
也就是说:
- 它不建立真实 MQTT 连接
- 不启动 broker
- 不走协议层编码与解码
- 不依赖底层 adapter
它测的是哪一段?
- 路由是否正确
- topic 是否匹配到 listener
- payload converter 是否被正确选中
- interceptor 是否按预期执行
- error strategy 是否被调用
这也是为什么 README 里明确说:
MqttTestTemplate.simulateIncoming(...)是 router 级快速测试工具,不是完整协议模拟器。
这个边界特别重要,因为它决定了你应该拿它做什么,不该拿它做什么。
适合它的场景
- listener 路由规则测试
- payload 转换测试
- headers 透传测试
- interceptor 链测试
- error handling 逻辑测试
不适合它的场景
- 真实客户端连接 broker 的测试
- 协议参数兼容性测试
- adapter 连接与发布行为测试
- 嵌入式 broker 网络交互测试
设计决策:
MqttTestTemplate没有试图模拟完整 MQTT 协议,而是明确把自己限制在 router 级测试入口。这样做的重点,是让高频测试足够快、足够稳定,同时避免做一个"看起来真实、实际上半真半假的协议模拟器"。
四、MqttTestTemplate 实际覆盖了哪些链路?
如果沿着源码往下看,它覆盖的其实正是前面几篇讲过的核心内链:
MqttMessageRouterMqttListenerRegistryPayloadConverterListenerInvokerMqttMessageInterceptorErrorHandlingStrategy
这也是为什么前面第 2 到第 5 篇那些核心行为,很多都特别适合用 router 级快测来验证。
比如 MqttTestTemplateTest 当前验证的就是:
simulateIncoming("primary", "devices/1/status", "online")会把字符串转成 UTF-8 bytes 再交给 router- 二进制 payload 和 headers 也会被原样交给 router
这些测试看起来简单,但意义很明确:
- 工具本身的行为边界是稳定的
- 快测入口的数据形状是可控的
也就是说,MqttTestTemplate 在测试体系里的角色,不是"替代所有测试",而是给框架内部那条最核心的消息处理主链,提供一个低成本、高频率的验证入口。
五、为什么还要有 @EnableMqttPlusTest 和 embedded broker?
如果只有 MqttTestTemplate,那测试会很快,但框架仍然缺少另一类非常重要的验证:
- Spring 测试上下文里这些 bean 到底能不能真正协作起来
- 真实 MQTT 客户端能不能连上一个 broker 并完成收发
- properties 注入、embedded broker 生命周期和 Spring 容器关闭这些边界是否稳定
这也是 @EnableMqttPlusTest 存在的原因。
它本身做了两件事:
@Import(MqttTestConfiguration.class),注册MqttTestTemplatebean@ContextConfiguration(initializers = EmbeddedMqttBrokerInitializer.class),在 Spring 上下文初始化时拉起 embedded broker 并注入测试配置
于是这条测试链就变成:
- Spring 容器起来
- embedded broker 启动
- 测试配置里自动注入
mqtt-plus.brokers.primary.* MqttTestTemplatebean 可直接用于 router 级辅助验证- 如果当前测试上下文本身还引入了 starter,那么 starter 也可以继续消费这批属性完成自动装配
- 你也可以拿真实 MQTT client 去连接这个 broker 验证协议链
这就比 router 级快测更接近真实运行环境。
六、EmbeddedMqttBrokerInitializer 真正做了哪些关键工作?
这一层特别值得讲,因为它不是简单"new 一个 broker"。
EmbeddedMqttBrokerInitializer 当前会:
- 调用
EmbeddedMqttBroker.startDefault()拉起一个默认 embedded broker - 动态分配端口,而不是写死端口
- 把以下属性注入 Spring 环境:
mqtt-plus.brokers.primary.hostmqtt-plus.brokers.primary.portmqtt-plus.brokers.primary.client-id
- 把 broker 作为单例注册进容器:
embeddedMqttBroker - 在
ContextClosedEvent里关闭 broker
这里的关键不是"起了 broker",而是:
- broker 生命周期和 Spring 测试上下文绑定在一起
- 测试端口不冲突
- 在包含 starter 的测试上下文里,自动装配也可以直接吃到这些动态注入的配置
所以更准确地说,@EnableMqttPlusTest 本身提供的是 embedded broker 和测试辅助配置;如果测试上下文同时引入 starter,它也能让 starter 一起参与验证,但这不是注解单独就自动完成的事情。
@EnableMqttPlusTest
EmbeddedMqttBrokerInitializer
Start EmbeddedMqttBroker
Inject mqtt-plus.brokers.primary.* properties
MqttTestConfiguration provides MqttTestTemplate
Test uses Spring beans
如果测试上下文同时引入 starter,这些注入进去的 broker 配置还会进入另一条验证路径:
Injected mqtt-plus.brokers.primary.* properties
Starter binds broker properties
Adapters / router / template initialized
Test uses real MQTT client or starter-managed beans
这两张图其实解释了第 9 篇最重要的差异:
- router 级快测是"切进框架内部"
- embedded broker 测试是"让整个 Spring 装配和协议接入真正跑起来"
七、为什么说这两种测试方式是互补,而不是替代关系?
这里最容易掉进的误区,就是觉得:
- 有 embedded broker,就不需要
MqttTestTemplate - 或者有
MqttTestTemplate,就没必要再起 broker
其实两种说法都不对。
1. 如果所有测试都走 embedded broker
问题会是:
- 启动成本更高
- 测试速度更慢
- 出问题时更难定位,到底是协议链、starter 装配还是 router 逻辑出了问题
2. 如果所有测试都只走 MqttTestTemplate
问题会是:
- 你永远验证不到真实 MQTT 客户端和 broker 的交互
- 也看不到 embedded broker 生命周期和 Spring 配置注入是否正常
- adapter 与协议层边界永远没有真实覆盖
所以 mqtt-plus 当前的测试设计,其实是在做一个非常典型的工程权衡:
- 高密度逻辑验证,走快测
- 更接近系统真实边界的验证,走 embedded broker 测试
这也解释了为什么 README 里会直接用 "two complementary styles" 这种表述,而不是说"推荐其中一个"。
设计决策: mqtt-plus 没有把测试体系押宝在单一风格上,而是明确把
MqttTestTemplate和@EnableMqttPlusTest设计成互补关系。这样做的重点,是让框架既能保持高频快测能力,又能保留对真实协议链和 Spring 装配链的验证能力。
八、当前测试体系的边界在哪里?
这一篇如果不讲边界,就很容易让读者误解为"已经什么都测到了"。
当前这套测试体系已经覆盖得很有层次,但边界也很清楚。
1. MqttTestTemplate 不是协议模拟器
这点 README 已经写得很明确了。
它不会验证:
- 网络层行为
- 底层 client/broker 交互
- 真正的 QoS 协议语义
- adapter 的连接与回调实现
2. embedded broker 测试也不是生产环境完全镜像
当前 EmbeddedMqttBroker 使用的是内存型 Moquette 配置:
allow_anonymous=truepersistence_enabled=false
这意味着它更适合测试:
- 基本协议连通性
- Spring 上下文和 broker 协同
- 本地嵌入式验证
而不是模拟生产环境里所有认证、持久化和集群语义。
3. 真正更完整的链路验证仍需要 integration / sample smoke tests
从仓库整体结构和前面的文章也能看出来,starter、core、sample 还有更高一层的集成测试和 smoke test。
所以 mqtt-plus-test 更准确的定位是:
- 覆盖"比单元测试更真实、比端到端测试更轻量"的中间层
这正是它最有价值的地方。
九、小结
第 9 篇真正想讲清楚的,不是"mqtt-plus-test 里有哪些类",而是它背后的测试分层逻辑:
MqttTestTemplate提供 router 级快测入口,适合高频验证核心消息处理链@EnableMqttPlusTest通过 embedded broker 提供了更接近真实链路的 Spring 测试基础设施;如果测试上下文包含 starter,它也能把 Spring 装配链一起纳入验证范围- 两者不是竞争关系,而是覆盖不同成本、不同真实度的测试层级
也正因为这套设计成立,mqtt-plus 才不用在"快"和"真"之间做单选题。
它做的其实是一种更工程化的取舍:
- 能快速验证的地方,就不要硬拉真 broker
- 需要验证真实协议和上下文协作的地方,也不要只靠 mock 自我安慰
下一篇会收束整个系列,回到更高一层的问题:mqtt-plus 到底是如何从一个内部项目里的 MQTT 能力,抽取成一个可以公开维护的开源框架。
系列导航
本文是 mqtt-plus 架构解析 系列的第 9/10 篇。
| # | 主题 | 链接 |
|---|---|---|
| 1 | 总览:分层架构与设计哲学 | 链接 |
| 2 | 消息路由:一条 MQTT 消息如何到达你的 @MqttListener |
链接 |
| 3 | Payload 序列化与反序列化:双链设计的取舍 | 链接 |
| 4 | 拦截器链:MqttMessageInterceptor 的扩展点设计 |
链接 |
| 5 | 错误处理:ErrorAction 聚合策略的设计逻辑 |
链接 |
| 6 | 多 Broker 管理:如何让一个应用同时连接多个 MQTT 服务 | 链接 |
| 7 | 动态订阅与重连恢复:Reconciler 的协调机制 |
链接 |
| 8 | Spring Boot 自动装配:零件是怎么被粘合起来的 | 链接 |
| 9 | 测试体系:MqttTestTemplate 与 EmbeddedBroker 的设计 |
本文 |
| 10 | 从内部项目到开源框架:mqtt-plus 的抽取过程与决策 | 链接 |