From c1fddc8483f6587aa456502248570d641d0bdec9 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 3 May 2026 09:15:45 +0800 Subject: [PATCH] ✨ feat(mes): 增加 IotDeviceMessageServiceImpl 的单元测试,涵盖消息处理和数据库交互逻辑 --- yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java | 204 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 198 insertions(+), 6 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java index 8ee5292..78bf74d 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java @@ -1,28 +1,43 @@ package cn.iocoder.yudao.module.iot.service.device.message; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.iocoder.yudao.framework.common.exception.ServiceException; +import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO; import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper; import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import java.time.LocalDateTime; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.*; /** * {@link IotDeviceMessageServiceImpl} 的单元测试 * + * 注:TDengine 数据源没有 embedded 替代,mapper 与依赖 service 走 mock; + * handleUpstreamDeviceMessage 与 sendDeviceMessage 下行成功路径依赖 SpringUtil.getBean 的自调用 + * createDeviceLogAsync,更适合放到集成测试,本类不展开。 + * * @author 芋道源码 */ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { @@ -41,17 +56,45 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { @Mock private IotDeviceMessageProducer deviceMessageProducer; + // ========== defineDeviceMessageStable ========== + + @Test + public void testDefineDeviceMessageStable_whenTableExists_skipCreate() { + // 准备:showSTable 返回非空 → 表已存在 + when(deviceMessageMapper.showSTable()).thenReturn("device_message"); + + // 调用 + service.defineDeviceMessageStable(); + + // 断言:跳过 createSTable + verify(deviceMessageMapper, never()).createSTable(); + } + + @Test + public void testDefineDeviceMessageStable_whenTableMissing_create() { + // 准备:showSTable 返回空 → 表不存在 + when(deviceMessageMapper.showSTable()).thenReturn(""); + + // 调用 + service.defineDeviceMessageStable(); + + // 断言:触发 createSTable + verify(deviceMessageMapper, times(1)).createSTable(); + } + + // ========== createDeviceLogAsync ========== + @Test public void testCreateDeviceLogAsync_tsFallback_whenNull() { // 准备:构造一条 ts 为 null 的消息 - IotDeviceMessage message = buildMessage(); + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); long before = System.currentTimeMillis(); // 调用 service.createDeviceLogAsync(message); long after = System.currentTimeMillis(); - // 断言:mapper.insert 接收到的 messageDO 已经被填上 ts,值在 [before, after] 区间 + // 断言:mapper.insert 接收到的 messageDO 已被填上 ts,值在 [before, after] 区间 ArgumentCaptor captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class); verify(deviceMessageMapper).insert(captor.capture()); Long actualTs = captor.getValue().getTs(); @@ -63,7 +106,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { @Test public void testCreateDeviceLogAsync_swallowMapperException() { // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛 - IotDeviceMessage message = buildMessage(); + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); doThrow(new RuntimeException("DB unavailable")).when(deviceMessageMapper).insert(any()); // 调用 & 断言 @@ -71,14 +114,163 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { verify(deviceMessageMapper).insert(any(IotDeviceMessageDO.class)); } - /** 构造一条最简属性上报消息 */ - private IotDeviceMessage buildMessage() { + // ========== sendDeviceMessage ========== + + @Test + public void testSendDeviceMessage_upstream_publishToProducer() { + // 准备:上行消息(PROPERTY_POST) + IotDeviceDO device = buildDevice(); + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + + // 调用 + IotDeviceMessage result = service.sendDeviceMessage(message, device); + + // 断言:走 producer.sendDeviceMessage,不进入下行链路 + assertSame(message, result); + verify(deviceMessageProducer, times(1)).sendDeviceMessage(message); + verify(deviceMessageProducer, never()).sendDeviceMessageToGateway(any(), any()); + verify(devicePropertyService, never()).getDeviceServerId(any()); + } + + @Test + public void testSendDeviceMessage_downstream_serverIdMissing_throwException() { + // 准备:下行消息(SERVICE_INVOKE);devicePropertyService 也查不到 serverId + IotDeviceDO device = buildDevice(); + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod()); + when(devicePropertyService.getDeviceServerId(device.getId())).thenReturn(null); + + // 调用 & 断言:抛 DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL + ServiceException ex = assertThrows(ServiceException.class, + () -> service.sendDeviceMessage(message, device)); + assertEquals(1_050_003_007, ex.getCode().intValue()); + verify(deviceMessageProducer, never()).sendDeviceMessageToGateway(any(), any()); + } + + // ========== getDeviceMessagePage ========== + + @Test + public void testGetDeviceMessagePage_normal() { + // 准备 + IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1"); + Page page = new Page<>(1, 10, 1L); + page.setRecords(Collections.singletonList(record)); + when(deviceMessageMapper.selectPage(any(), eq(reqVO))).thenReturn(page); + + // 调用 + PageResult result = service.getDeviceMessagePage(reqVO); + + // 断言 + assertEquals(1L, result.getTotal()); + assertEquals(1, result.getList().size()); + assertEquals("msg-1", result.getList().get(0).getId()); + } + + @Test + public void testGetDeviceMessagePage_whenTableMissing_returnEmpty() { + // 准备:mapper 抛 "Table does not exist" → 视为表未创建,返回空结果 + IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + when(deviceMessageMapper.selectPage(any(), any())) + .thenThrow(new RuntimeException("Table does not exist")); + + // 调用 + PageResult result = service.getDeviceMessagePage(reqVO); + + // 断言 + assertEquals(0L, result.getTotal()); + assertTrue(result.getList().isEmpty()); + } + + @Test + public void testGetDeviceMessagePage_otherException_rethrow() { + // 准备:mapper 抛非「表不存在」的异常 → 应向上抛 + IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO(); + reqVO.setPageNo(1); + reqVO.setPageSize(10); + when(deviceMessageMapper.selectPage(any(), any())) + .thenThrow(new RuntimeException("Connection refused")); + + // 调用 & 断言 + assertThrows(RuntimeException.class, () -> service.getDeviceMessagePage(reqVO)); + } + + // ========== getDeviceMessageListByRequestIdsAndReply ========== + + @Test + public void testGetDeviceMessageListByRequestIdsAndReply_emptyIds_returnEmpty() { + // 调用 & 断言:requestIds 为空直接返回空列表,不查 DB + List result = service.getDeviceMessageListByRequestIdsAndReply( + 1L, Collections.emptyList(), true); + + assertTrue(result.isEmpty()); + verify(deviceMessageMapper, never()).selectListByRequestIdsAndReply(any(), any(), any()); + } + + @Test + public void testGetDeviceMessageListByRequestIdsAndReply_normal_delegateToMapper() { + // 准备 + List requestIds = Collections.singletonList("req-1"); + IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1"); + when(deviceMessageMapper.selectListByRequestIdsAndReply(1L, requestIds, true)) + .thenReturn(Collections.singletonList(record)); + + // 调用 + List result = service.getDeviceMessageListByRequestIdsAndReply( + 1L, requestIds, true); + + // 断言 + assertEquals(1, result.size()); + assertEquals("msg-1", result.get(0).getId()); + } + + // ========== getDeviceMessageCount ========== + + @Test + public void testGetDeviceMessageCount_whenCreateTimeNull_passNullToMapper() { + // 准备 + when(deviceMessageMapper.selectCountByCreateTime(isNull())).thenReturn(123L); + + // 调用 + Long count = service.getDeviceMessageCount(null); + + // 断言 + assertEquals(123L, count); + verify(deviceMessageMapper).selectCountByCreateTime(isNull()); + } + + @Test + public void testGetDeviceMessageCount_withCreateTime_passEpochMilli() { + // 准备:service 内部用 LocalDateTimeUtil.toEpochMilli 做转换,断言时也用同函数得到期望值 + LocalDateTime createTime = LocalDateTime.of(2026, 1, 1, 0, 0); + long expectedMs = LocalDateTimeUtil.toEpochMilli(createTime); + when(deviceMessageMapper.selectCountByCreateTime(expectedMs)).thenReturn(456L); + + // 调用 + Long count = service.getDeviceMessageCount(createTime); + + // 断言 + assertEquals(456L, count); + } + + // ========== 辅助方法 ========== + + /** 构造一条最简消息(指定 method 决定上下行分支) */ + private IotDeviceMessage buildMessage(String method) { IotDeviceMessage message = new IotDeviceMessage(); message.setId("msg-1"); message.setDeviceId(2L); - message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); + message.setMethod(method); message.setParams(new HashMap<>()); return message; } + /** 构造最简设备 */ + private IotDeviceDO buildDevice() { + return IotDeviceDO.builder().id(2L).build(); + } + } -- libgit2 0.21.4