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 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 6 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  7 +import cn.iocoder.yudao.module.iot.controller.admin.device.vo.message.IotDeviceMessagePageReqVO;
4 8 import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
5 9 import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
6 10 import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
  11 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
7 12 import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
8 13 import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper;
9 14 import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
10 15 import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
11 16 import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService;
  17 +import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
12 18 import org.junit.jupiter.api.Test;
13 19 import org.mockito.ArgumentCaptor;
14 20 import org.mockito.InjectMocks;
15 21 import org.mockito.Mock;
16 22  
  23 +import java.time.LocalDateTime;
  24 +import java.util.Collections;
17 25 import java.util.HashMap;
  26 +import java.util.List;
18 27  
19 28 import static org.junit.jupiter.api.Assertions.*;
20 29 import static org.mockito.ArgumentMatchers.any;
  30 +import static org.mockito.ArgumentMatchers.eq;
  31 +import static org.mockito.ArgumentMatchers.isNull;
21 32 import static org.mockito.Mockito.*;
22 33  
23 34 /**
24 35 * {@link IotDeviceMessageServiceImpl} 的单元测试
25 36 *
  37 + * 注:TDengine 数据源没有 embedded 替代,mapper 与依赖 service 走 mock;
  38 + * handleUpstreamDeviceMessage 与 sendDeviceMessage 下行成功路径依赖 SpringUtil.getBean 的自调用
  39 + * createDeviceLogAsync,更适合放到集成测试,本类不展开。
  40 + *
26 41 * @author 芋道源码
27 42 */
28 43 public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
... ... @@ -41,17 +56,45 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
41 56 @Mock
42 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 87 @Test
45 88 public void testCreateDeviceLogAsync_tsFallback_whenNull() {
46 89 // 准备:构造一条 ts 为 null 的消息
47   - IotDeviceMessage message = buildMessage();
  90 + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
48 91 long before = System.currentTimeMillis();
49 92  
50 93 // 调用
51 94 service.createDeviceLogAsync(message);
52 95 long after = System.currentTimeMillis();
53 96  
54   - // 断言:mapper.insert 接收到的 messageDO 已经被填上 ts,值在 [before, after] 区间
  97 + // 断言:mapper.insert 接收到的 messageDO 已被填上 ts,值在 [before, after] 区间
55 98 ArgumentCaptor<IotDeviceMessageDO> captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class);
56 99 verify(deviceMessageMapper).insert(captor.capture());
57 100 Long actualTs = captor.getValue().getTs();
... ... @@ -63,7 +106,7 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
63 106 @Test
64 107 public void testCreateDeviceLogAsync_swallowMapperException() {
65 108 // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛
66   - IotDeviceMessage message = buildMessage();
  109 + IotDeviceMessage message = buildMessage(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
67 110 doThrow(new RuntimeException("DB unavailable")).when(deviceMessageMapper).insert(any());
68 111  
69 112 // 调用 & 断言
... ... @@ -71,14 +114,163 @@ public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
71 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 263 IotDeviceMessage message = new IotDeviceMessage();
77 264 message.setId("msg-1");
78 265 message.setDeviceId(2L);
79   - message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
  266 + message.setMethod(method);
80 267 message.setParams(new HashMap<>());
81 268 return message;
82 269 }
83 270  
  271 + /** 构造最简设备 */
  272 + private IotDeviceDO buildDevice() {
  273 + return IotDeviceDO.builder().id(2L).build();
  274 + }
  275 +
84 276 }
... ...