Spring Boot 单元测试怎么写?物联网平台 MockMvc 接口测试与 Mockito 消息链路测试实战(附 JaCoCo 覆盖率报告)

真实运行截图:


一、功能都写完了,为什么还要写测试?

我这个物联网设备接入平台(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%+"更加分。

面试考点小结

  1. 测试金字塔分工 :逻辑分支 → Mockito 纯单元测试;跨层协作 → @SpringBootTest + MockMvc + H2 内存库;
  2. @MockBean 的用途:隔离外部依赖(MQTT Broker),测业务不测网络;
  3. ArgumentCaptor:断言"传给依赖的对象长什么样",而不只是"调用过没有";
  4. 回归测试:Bug 修完必须留一个用例钉死,防止复发;
  5. 覆盖率工具: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 真实源码并标注来源文件;截图为真实测试输出与报告画面。

相关推荐
LiuYanG2 小时前
物联网平台怎么给设备下发指令-SpringBoot下行命令与回执状态机实战
物联网
权球物联分享物联网连接服务2 小时前
AGV 机器人商业化落地验证,物联网卡保障产线交付稳定性!
物联网·机器人
码流子3 小时前
事件告警延迟拆解:从摄像头到手机通知,延迟卡在哪六个环节
数据库·人工智能·物联网
数字新视界3 小时前
机房环境监控系统的关键应用与发展趋势
大数据·物联网·信息可视化·信息与通信·一体化机柜
博纬智能RFID3 小时前
服装旗舰店防盗升级:RFID吊顶门禁如何实现无感防损?
物联网
话梅姐姐呀4 小时前
浅谈智能硬件 OTA
物联网
kiros_wang4 小时前
BLE蓝牙基础通信:扫描、连接、收发数据,IoT简易Demo
物联网
做萤石二次开发的哈哈13 小时前
连锁门店、园区、交通枢纽的视频如何统一汇集?应用模板的接入与分权管理详解
人工智能·物联网·音视频·二次开发·视频监控·蓝海aiot一站式工作台·萤石云开放平台
QYR_1117 小时前
机械制造用固体润滑涂层市场分析:2032年全球市场规模预计达8.78亿元,CAGR为6.2%
大数据·人工智能·物联网