真实运行截图:



一、功能都写完了,为什么还要写测试?
我这个物联网设备接入平台(Spring Boot 3.5 + EMQX + MySQL),从设备注册、MQTT 接入、消息入库到密钥轮换,功能主线已经跑通了。但每次面试话题走到"质量保障",我都心虚------功能是我手点出来的,谁保证我下次改一行代码不会把注册接口改挂?
测试在项目里的位置是这样的:接入层、数据层、安全层的功能代码是"存货",测试是"保险"。每次给设备加新字段、给消息处理加新分支,跑一遍测试就知道有没有把老功能弄坏。这在物联网领域尤其重要------设备的认证/ACL 逻辑改坏了,不是页面白屏,而是陌生设备能连进你的 Broker。
先交代结论:项目现在有 11 个测试类、37 个测试用例,mvn test 全绿(见第一张截图),整体行覆盖率 76%。下面讲这 37 个用例怎么组织的。
二、两类测试的分工:谁该连数据库,谁不该
我的测试分两类,对应测试金字塔的两层:
| 类型 | 代表 | 依赖 | 测什么 |
|---|---|---|---|
| 纯单元测试 | TelemetryMessageHandlerTest |
不起 Spring,Repository 全部 Mock | 消息处理的业务逻辑分支 |
| Web 层测试 | DeviceControllerTest、AdminTokenInterceptorTest 等 |
@SpringBootTest + H2 内存库 |
HTTP 接口从过滤器到数据库的整条链路 |
分工原则一句话:逻辑分支多的用纯单元测试跑得快,跨层协作的用 MockMvc 测真实行为。消息处理类里全是 if/else 分支(未知设备、productKey 不匹配、非法 JSON......),起一次 Spring 上下文要好几秒,纯单元测试毫秒级跑完;而"注册接口到底返回 201 还是 409"这种问题,Mock 不出来,必须让请求真穿过拦截器和 JPA。
三、消息链路纯单元测试:Mockito + ArgumentCaptor
被测的 TelemetryMessageHandler 是 MQTT 消息的入口(来源 src\main\java\com\iothub\mqtt\TelemetryMessageHandler.java),核心逻辑:解析主题 up/{productKey}/{deviceId},校验设备存在且 productKey 匹配,再按 up/st 分流。
java
@Test
void telemetryMarksDeviceOnlineAndSavesDataPoint() {
when(deviceRepository.findById(1L)).thenReturn(Optional.of(device));
handler.handle("up/th-sensor/1", "{\"temp\":25.5,\"humi\":60}");
assertThat(device.getStatus()).isEqualTo("online");
verify(deviceRepository).save(device);
// ArgumentCaptor 抓住传给 Mock 的对象,逐字段断言
ArgumentCaptor<DataPoint> captor = ArgumentCaptor.forClass(DataPoint.class);
verify(dataPointRepository).save(captor.capture());
assertThat(captor.getValue().getDeviceId()).isEqualTo(1L);
assertThat(captor.getValue().getProductKey()).isEqualTo("th-sensor");
assertThat(captor.getValue().getPayload()).contains("25.5");
}
(来源 src\test\java\com\iothub\mqtt\TelemetryMessageHandlerTest.java,类上用 @ExtendWith(MockitoExtension.class),不起 Spring 容器)
这个类一共 5 个用例,专门冲着异常分支 去的:未知设备直接丢弃、productKey 不匹配丢弃、非法 JSON 丢弃、状态消息只改设备状态不存数据点。为什么这么设计?因为消息链路的正常路径只有一条,出事的永远在异常路径------伪造的 productKey、断电设备发来的脏数据,都得在这里拦住。测试里把每个"丢弃"分支都钉死,以后谁改这段代码漏了校验,测试立刻红。
四、接口层测试:MockMvc 从拦截器一路测到数据库
注册接口是平台最核心的入口,DeviceControllerTest 三个用例把三种结局都锁死了:
java
@Test
void registerSuccessReturnsSecretOnce() throws Exception {
// ... post /api/devices 断言 201 + code=0 + secret/clientId 非空 ...
// 从响应里抠出明文密钥,再验证库里存的确实是哈希而不是明文
Device saved = deviceRepository.findAll().stream()
.filter(d -> "test-温湿度-001".equals(d.getDeviceName()))
.findFirst().orElseThrow();
assertThat(saved.getSecretHash())
.isNotEqualTo(secret)
.isEqualTo(DeviceService.sha256(secret));
}
(来源 src\test\java\com\iothub\device\DeviceControllerTest.java)
这个用例我最得意的地方不是测了 201,而是最后三行:从响应里拿到明文密钥,再断言数据库里存的不是明文、且等于 SHA-256(明文)。接口测试的价值就在这------它能穿透到数据库验证副作用,这是 Postman 手点给不了的。
另一个套路是"写操作测权限":AdminTokenInterceptorTest 用 5 个用例验证管理令牌(无令牌 401、错令牌 401、对令牌放行、GET 公开、EMQX 回调豁免)。注意类上有 @MockBean private MqttConnection mqttConnection------起 Spring 上下文但不让它真连 Broker,这是物联网项目 Web 测试的关键技巧:测接口不测网络。
五、测试抓出来的两个真 Bug
测试不是摆设,这两个真 Bug 就是它抓的:
Bug 1:浏览器要个 favicon.ico,日志里炸出一条 500。 ErrorResponseTest 里前两个用例是专门的回归测试------之前没单独处理 NoResourceFoundException,任何浏览器访问 /favicon.ico 都会被全局异常兜底捕获返回 500,还刷一条 ERROR 堆栈。修完之后测试钉死:现在必须返回 404 + code=404。
Bug 2:MockMvc 默认按 ISO-8859-1 解码响应,中文断言全乱码。 测静态看板页时 getContentAsString() 拿到的中文全是问号,断言"设备实时监控看板"永远失败。解法是显式传编码:getContentAsString(StandardCharsets.UTF_8)(ErrorResponseTest 源码里有注释标注)。这种坑不写测试你根本遇不到,因为浏览器会自己猜编码。
六、跑测试 + JaCoCo:拿真实数字,别拍脑袋
跑测试和覆盖率不需要改 pom.xml,命令行直接拼插件目标:
bash
mvn org.jacoco:jacoco-maven-plugin:0.8.12:prepare-agent test org.jacoco:jacoco-maven-plugin:0.8.12:report
今天的真实结果(见前两张截图):Tests run: 37, Failures: 0, Errors: 0, Skipped: 0,BUILD SUCCESS 。覆盖率报告在 target\site\jacoco\index.html,真实数字:
| 包 | 行覆盖 | 说明 |
|---|---|---|
| com.iothub.device | 96% | 注册/生命周期,MockMvc 覆盖最全 |
| com.iothub.common | 94% | 拦截器/异常处理,回归测试钉死 |
| com.iothub.data | 89% | 查询接口与保留任务 |
| com.iothub.mqtt | 75% | 认证/ACL 很高,MqttConnection 重连逻辑低 |
| com.iothub.ws | 13% | WebSocket 推送循环,基本没测 |
| 整体 | 76% | 指令覆盖 79%、分支覆盖 71% |
说句实话:ws 包那 13% 我不打算现在补。WebSocket 推送是定时循环+会话广播,Mock 它的成本远超收益,我的取舍是用真实浏览器开两个标签页人工验收过(M4 里程碑那次)。覆盖率是体检报告不是 KPI------它最大的用处是告诉你风险在哪,而不是逼你凑 100%。面试里能讲清"哪块没测、为什么暂不补",比一句"覆盖率 90%+"更加分。
面试考点小结
- 测试金字塔分工 :逻辑分支 → Mockito 纯单元测试;跨层协作 →
@SpringBootTest+ MockMvc + H2 内存库; @MockBean的用途:隔离外部依赖(MQTT Broker),测业务不测网络;- ArgumentCaptor:断言"传给依赖的对象长什么样",而不只是"调用过没有";
- 回归测试:Bug 修完必须留一个用例钉死,防止复发;
- 覆盖率工具:JaCoCo 不改 pom 也能用 CLI 挂上;覆盖率看分布不看总分。
写在最后
我是软件工程在读,正在从零搭一个物联网设备接入平台(Spring Boot + EMQX + MySQL),这篇是质量保障这条支线的第一篇。写这篇之前我以为测试是"任务",写完发现那两个真 Bug 已经回本了。下一篇计划写 EMQX 共享订阅或 Kafka 解耦。你项目里的测试覆盖率是多少?评论区聊一句,比点赞更让我高兴。
本文所有数据均为本机实测(2026-10-10):mvn test 37 个用例全绿、JaCoCo 覆盖率来自 target\site\jacoco\ 真实报告;代码均摘自项目 D:\dev\iot-device-platform 真实源码并标注来源文件;截图为真实测试输出与报告画面。