Commit c1fddc8483f6587aa456502248570d641d0bdec9

Authored by YunaiV
1 parent ac9c11c0

✨ feat(mes): 增加 IotDeviceMessageServiceImpl 的单元测试,涵盖消息处理和数据库交互逻辑

yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java
1 package cn.iocoder.yudao.module.iot.service.device.message; 1 package cn.iocoder.yudao.module.iot.service.device.message;
2 2
  3 +import cn.hutool.core.date.LocalDateTimeUtil;
  4 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  5 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
3 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; 6 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  7 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO;
4 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; 8 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer; 10 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
7 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO; 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
8 import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper; 13 import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper;
9 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService; 14 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
10 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService; 15 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
11 import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService; 16 import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService;
  17 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
12 import org.junit.jupiter.api.Test; 18 import org.junit.jupiter.api.Test;
13 import org.mockito.ArgumentCaptor; 19 import org.mockito.ArgumentCaptor;
14 import org.mockito.InjectMocks; 20 import org.mockito.InjectMocks;
15 import org.mockito.Mock; 21 import org.mockito.Mock;
16 22
  23 +import java.time.LocalDateTime;
  24 +import java.util.Collections;
17 import java.util.HashMap; 25 import java.util.HashMap;
  26 +import java.util.List;
18 27
19 import static org.junit.jupiter.api.Assertions.*; 28 import static org.junit.jupiter.api.Assertions.*;
20 import static org.mockito.ArgumentMatchers.any; 29 import static org.mockito.ArgumentMatchers.any;
  30 +import static org.mockito.ArgumentMatchers.eq;
  31 +import static org.mockito.ArgumentMatchers.isNull;
21 import static org.mockito.Mockito.*; 32 import static org.mockito.Mockito.*;
22 33
23 /** 34 /**
24 * {@link IotDeviceMessageServiceImpl} 的单元测试 35 * {@link IotDeviceMessageServiceImpl} 的单元测试
25 * 36 *
  37 + * 注:TDengine 数据源没有 embedded 替代,mapper 与依赖 service 走 mock;
  38 + * handleUpstreamDeviceMessage 与 sendDeviceMessage 下行成功路径依赖 SpringUtil.getBean 的自调用
  39 + * createDeviceLogAsync,更适合放到集成测试,本类不展开。
  40 + *
26 * @author 芋道源码 41 * @author 芋道源码
27 */ 42 */
28 public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { 43 public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
@@ -41,17 +56,45 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { @@ -41,17 +56,45 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
41 @Mock 56 @Mock
42 private IotDeviceMessageProducer deviceMessageProducer; 57 private IotDeviceMessageProducer deviceMessageProducer;
43 58
  59 + // ========== defineDeviceMessageStable ==========
  60 +
  61 + @Test
  62 + public void testDefineDeviceMessageStable_whenTableExists_skipCreate() {
  63 + // 准备:showSTable 返回非空 → 表已存在
  64 + when(deviceMessageMapper.showSTable()).thenReturn("device_message");
  65 +
  66 + // 调用
  67 + service.defineDeviceMessageStable();
  68 +
  69 + // 断言:跳过 createSTable
  70 + verify(deviceMessageMapper, never()).createSTable();
  71 + }
  72 +
  73 + @Test
  74 + public void testDefineDeviceMessageStable_whenTableMissing_create() {
  75 + // 准备:showSTable 返回空 → 表不存在
  76 + when(deviceMessageMapper.showSTable()).thenReturn("");
  77 +
  78 + // 调用
  79 + service.defineDeviceMessageStable();
  80 +
  81 + // 断言:触发 createSTable
  82 + verify(deviceMessageMapper, times(1)).createSTable();
  83 + }
  84 +
  85 + // ========== createDeviceLogAsync ==========
  86 +
44 @Test 87 @Test
45 public void testCreateDeviceLogAsync_tsFallback_whenNull() { 88 public void testCreateDeviceLogAsync_tsFallback_whenNull() {
46 // 准备:构造一条 ts 为 null 的消息 89 // 准备:构造一条 ts 为 null 的消息
47 - IotDeviceMessage message = buildMessage(); 90 + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
48 long before = System.currentTimeMillis(); 91 long before = System.currentTimeMillis();
49 92
50 // 调用 93 // 调用
51 service.createDeviceLogAsync(message); 94 service.createDeviceLogAsync(message);
52 long after = System.currentTimeMillis(); 95 long after = System.currentTimeMillis();
53 96
54 - // 断言:mapper.insert 接收到的 messageDO 已经被填上 ts,值在 [before, after] 区间 97 + // 断言:mapper.insert 接收到的 messageDO 已被填上 ts,值在 [before, after] 区间
55 ArgumentCaptor<IotDeviceMessageDO> captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class); 98 ArgumentCaptor<IotDeviceMessageDO> captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class);
56 verify(deviceMessageMapper).insert(captor.capture()); 99 verify(deviceMessageMapper).insert(captor.capture());
57 Long actualTs = captor.getValue().getTs(); 100 Long actualTs = captor.getValue().getTs();
@@ -63,7 +106,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { @@ -63,7 +106,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
63 @Test 106 @Test
64 public void testCreateDeviceLogAsync_swallowMapperException() { 107 public void testCreateDeviceLogAsync_swallowMapperException() {
65 // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛 108 // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛
66 - IotDeviceMessage message = buildMessage(); 109 + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
67 doThrow(new RuntimeException("DB unavailable")).when(deviceMessageMapper).insert(any()); 110 doThrow(new RuntimeException("DB unavailable")).when(deviceMessageMapper).insert(any());
68 111
69 // 调用 & 断言 112 // 调用 & 断言
@@ -71,14 +114,163 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest { @@ -71,14 +114,163 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
71 verify(deviceMessageMapper).insert(any(IotDeviceMessageDO.class)); 114 verify(deviceMessageMapper).insert(any(IotDeviceMessageDO.class));
72 } 115 }
73 116
74 - /** 构造一条最简属性上报消息 */  
75 - private IotDeviceMessage buildMessage() { 117 + // ========== sendDeviceMessage ==========
  118 +
  119 + @Test
  120 + public void testSendDeviceMessage_upstream_publishToProducer() {
  121 + // 准备:上行消息(PROPERTY_POST)
  122 + IotDeviceDO device = buildDevice();
  123 + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
  124 +
  125 + // 调用
  126 + IotDeviceMessage result = service.sendDeviceMessage(message, device);
  127 +
  128 + // 断言:走 producer.sendDeviceMessage,不进入下行链路
  129 + assertSame(message, result);
  130 + verify(deviceMessageProducer, times(1)).sendDeviceMessage(message);
  131 + verify(deviceMessageProducer, never()).sendDeviceMessageToGateway(any(), any());
  132 + verify(devicePropertyService, never()).getDeviceServerId(any());
  133 + }
  134 +
  135 + @Test
  136 + public void testSendDeviceMessage_downstream_serverIdMissing_throwException() {
  137 + // 准备:下行消息(SERVICE_INVOKE);devicePropertyService 也查不到 serverId
  138 + IotDeviceDO device = buildDevice();
  139 + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod());
  140 + when(devicePropertyService.getDeviceServerId(device.getId())).thenReturn(null);
  141 +
  142 + // 调用 & 断言:抛 DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL
  143 + ServiceException ex = assertThrows(ServiceException.class,
  144 + () -> service.sendDeviceMessage(message, device));
  145 + assertEquals(1_050_003_007, ex.getCode().intValue());
  146 + verify(deviceMessageProducer, never()).sendDeviceMessageToGateway(any(), any());
  147 + }
  148 +
  149 + // ========== getDeviceMessagePage ==========
  150 +
  151 + @Test
  152 + public void testGetDeviceMessagePage_normal() {
  153 + // 准备
  154 + IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO();
  155 + reqVO.setPageNo(1);
  156 + reqVO.setPageSize(10);
  157 + IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1");
  158 + Page<IotDeviceMessageDO> page = new Page<>(1, 10, 1L);
  159 + page.setRecords(Collections.singletonList(record));
  160 + when(deviceMessageMapper.selectPage(any(), eq(reqVO))).thenReturn(page);
  161 +
  162 + // 调用
  163 + PageResult<IotDeviceMessageDO> result = service.getDeviceMessagePage(reqVO);
  164 +
  165 + // 断言
  166 + assertEquals(1L, result.getTotal());
  167 + assertEquals(1, result.getList().size());
  168 + assertEquals("msg-1", result.getList().get(0).getId());
  169 + }
  170 +
  171 + @Test
  172 + public void testGetDeviceMessagePage_whenTableMissing_returnEmpty() {
  173 + // 准备:mapper 抛 "Table does not exist" → 视为表未创建,返回空结果
  174 + IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO();
  175 + reqVO.setPageNo(1);
  176 + reqVO.setPageSize(10);
  177 + when(deviceMessageMapper.selectPage(any(), any()))
  178 + .thenThrow(new RuntimeException("Table does not exist"));
  179 +
  180 + // 调用
  181 + PageResult<IotDeviceMessageDO> result = service.getDeviceMessagePage(reqVO);
  182 +
  183 + // 断言
  184 + assertEquals(0L, result.getTotal());
  185 + assertTrue(result.getList().isEmpty());
  186 + }
  187 +
  188 + @Test
  189 + public void testGetDeviceMessagePage_otherException_rethrow() {
  190 + // 准备:mapper 抛非「表不存在」的异常 → 应向上抛
  191 + IotDeviceMessagePageReqVO reqVO = new IotDeviceMessagePageReqVO();
  192 + reqVO.setPageNo(1);
  193 + reqVO.setPageSize(10);
  194 + when(deviceMessageMapper.selectPage(any(), any()))
  195 + .thenThrow(new RuntimeException("Connection refused"));
  196 +
  197 + // 调用 & 断言
  198 + assertThrows(RuntimeException.class, () -> service.getDeviceMessagePage(reqVO));
  199 + }
  200 +
  201 + // ========== getDeviceMessageListByRequestIdsAndReply ==========
  202 +
  203 + @Test
  204 + public void testGetDeviceMessageListByRequestIdsAndReply_emptyIds_returnEmpty() {
  205 + // 调用 & 断言:requestIds 为空直接返回空列表,不查 DB
  206 + List<IotDeviceMessageDO> result = service.getDeviceMessageListByRequestIdsAndReply(
  207 + 1L, Collections.emptyList(), true);
  208 +
  209 + assertTrue(result.isEmpty());
  210 + verify(deviceMessageMapper, never()).selectListByRequestIdsAndReply(any(), any(), any());
  211 + }
  212 +
  213 + @Test
  214 + public void testGetDeviceMessageListByRequestIdsAndReply_normal_delegateToMapper() {
  215 + // 准备
  216 + List<String> requestIds = Collections.singletonList("req-1");
  217 + IotDeviceMessageDO record = new IotDeviceMessageDO().setId("msg-1");
  218 + when(deviceMessageMapper.selectListByRequestIdsAndReply(1L, requestIds, true))
  219 + .thenReturn(Collections.singletonList(record));
  220 +
  221 + // 调用
  222 + List<IotDeviceMessageDO> result = service.getDeviceMessageListByRequestIdsAndReply(
  223 + 1L, requestIds, true);
  224 +
  225 + // 断言
  226 + assertEquals(1, result.size());
  227 + assertEquals("msg-1", result.get(0).getId());
  228 + }
  229 +
  230 + // ========== getDeviceMessageCount ==========
  231 +
  232 + @Test
  233 + public void testGetDeviceMessageCount_whenCreateTimeNull_passNullToMapper() {
  234 + // 准备
  235 + when(deviceMessageMapper.selectCountByCreateTime(isNull())).thenReturn(123L);
  236 +
  237 + // 调用
  238 + Long count = service.getDeviceMessageCount(null);
  239 +
  240 + // 断言
  241 + assertEquals(123L, count);
  242 + verify(deviceMessageMapper).selectCountByCreateTime(isNull());
  243 + }
  244 +
  245 + @Test
  246 + public void testGetDeviceMessageCount_withCreateTime_passEpochMilli() {
  247 + // 准备:service 内部用 LocalDateTimeUtil.toEpochMilli 做转换,断言时也用同函数得到期望值
  248 + LocalDateTime createTime = LocalDateTime.of(2026, 1, 1, 0, 0);
  249 + long expectedMs = LocalDateTimeUtil.toEpochMilli(createTime);
  250 + when(deviceMessageMapper.selectCountByCreateTime(expectedMs)).thenReturn(456L);
  251 +
  252 + // 调用
  253 + Long count = service.getDeviceMessageCount(createTime);
  254 +
  255 + // 断言
  256 + assertEquals(456L, count);
  257 + }
  258 +
  259 + // ========== 辅助方法 ==========
  260 +
  261 + /** 构造一条最简消息(指定 method 决定上下行分支) */
  262 + private IotDeviceMessage buildMessage(String method) {
76 IotDeviceMessage message = new IotDeviceMessage(); 263 IotDeviceMessage message = new IotDeviceMessage();
77 message.setId("msg-1"); 264 message.setId("msg-1");
78 message.setDeviceId(2L); 265 message.setDeviceId(2L);
79 - message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod()); 266 + message.setMethod(method);
80 message.setParams(new HashMap<>()); 267 message.setParams(new HashMap<>());
81 return message; 268 return message;
82 } 269 }
83 270
  271 + /** 构造最简设备 */
  272 + private IotDeviceDO buildDevice() {
  273 + return IotDeviceDO.builder().id(2L).build();
  274 + }
  275 +
84 } 276 }