Commit ac9c11c00f5818cefa2d0cb4e125caac6fa19bd3

Authored by YunaiV
1 parent d668af19

✨ feat(iot): 添加 IotDeviceMessageServiceImpl 的单元测试,增强消息处理的可靠性

yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotWebSocketDataRuleAction.java
... ... @@ -97,11 +97,11 @@ public class IotWebSocketDataRuleAction extends
97 97 }
98 98 }
99 99  
100   - // 加锁原因:多线程并发发送消息时,多个线程可能同时检测到连接断开并尝试重连,
101   - // 使用锁 + 双重检查保证同一服务器地址只有一个线程执行重连操作
102 100 /**
103 101 * 使用锁进行重连,保证同一服务器地址的重连操作线程安全
104 102 *
  103 + * 加锁原因:多线程并发发送消息时,多个线程可能同时检测到连接断开并尝试重连,使用锁 + 双重检查保证同一服务器地址只有一个线程执行重连操作
  104 + *
105 105 * @param webSocketClient WebSocket 客户端
106 106 * @param config 配置信息
107 107 */
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.device.message;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
  5 +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
  6 +import cn.iocoder.yudao.module.iot.core.mq.producer.IotDeviceMessageProducer;
  7 +import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceMessageDO;
  8 +import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMessageMapper;
  9 +import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
  10 +import cn.iocoder.yudao.module.iot.service.device.property.IotDevicePropertyService;
  11 +import cn.iocoder.yudao.module.iot.service.ota.IotOtaTaskRecordService;
  12 +import org.junit.jupiter.api.Test;
  13 +import org.mockito.ArgumentCaptor;
  14 +import org.mockito.InjectMocks;
  15 +import org.mockito.Mock;
  16 +
  17 +import java.util.HashMap;
  18 +
  19 +import static org.junit.jupiter.api.Assertions.*;
  20 +import static org.mockito.ArgumentMatchers.any;
  21 +import static org.mockito.Mockito.*;
  22 +
  23 +/**
  24 + * {@link IotDeviceMessageServiceImpl} 的单元测试
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +public class IotDeviceMessageServiceImplTest extends BaseMockitoUnitTest {
  29 +
  30 + @InjectMocks
  31 + private IotDeviceMessageServiceImpl service;
  32 +
  33 + @Mock
  34 + private IotDeviceService deviceService;
  35 + @Mock
  36 + private IotDevicePropertyService devicePropertyService;
  37 + @Mock
  38 + private IotOtaTaskRecordService otaTaskRecordService;
  39 + @Mock
  40 + private IotDeviceMessageMapper deviceMessageMapper;
  41 + @Mock
  42 + private IotDeviceMessageProducer deviceMessageProducer;
  43 +
  44 + @Test
  45 + public void testCreateDeviceLogAsync_tsFallback_whenNull() {
  46 + // 准备:构造一条 ts 为 null 的消息
  47 + IotDeviceMessage message = buildMessage();
  48 + long before = System.currentTimeMillis();
  49 +
  50 + // 调用
  51 + service.createDeviceLogAsync(message);
  52 + long after = System.currentTimeMillis();
  53 +
  54 + // 断言:mapper.insert 接收到的 messageDO 已经被填上 ts,值在 [before, after] 区间
  55 + ArgumentCaptor<IotDeviceMessageDO> captor = ArgumentCaptor.forClass(IotDeviceMessageDO.class);
  56 + verify(deviceMessageMapper).insert(captor.capture());
  57 + Long actualTs = captor.getValue().getTs();
  58 + assertNotNull(actualTs, "ts 不应为空");
  59 + assertTrue(actualTs >= before && actualTs <= after,
  60 + "ts 应在调用前后区间内; 实际 = " + actualTs);
  61 + }
  62 +
  63 + @Test
  64 + public void testCreateDeviceLogAsync_swallowMapperException() {
  65 + // 准备:mapper.insert 抛异常,验证 @Async 方法内部 try/catch 兜底,不向上抛
  66 + IotDeviceMessage message = buildMessage();
  67 + doThrow(new RuntimeException("DB unavailable")).when(deviceMessageMapper).insert(any());
  68 +
  69 + // 调用 & 断言
  70 + assertDoesNotThrow(() -> service.createDeviceLogAsync(message));
  71 + verify(deviceMessageMapper).insert(any(IotDeviceMessageDO.class));
  72 + }
  73 +
  74 + /** 构造一条最简属性上报消息 */
  75 + private IotDeviceMessage buildMessage() {
  76 + IotDeviceMessage message = new IotDeviceMessage();
  77 + message.setId("msg-1");
  78 + message.setDeviceId(2L);
  79 + message.setMethod(IotDeviceMessageMethodEnum.PROPERTY_POST.getMethod());
  80 + message.setParams(new HashMap<>());
  81 + return message;
  82 + }
  83 +
  84 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/device/property/IotDevicePropertyServiceImplTest.java
... ... @@ -86,7 +86,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
86 86 service.saveDeviceProperty(device, message);
87 87  
88 88 // 断言:没有合法属性,不会写入 TDengine 与 Redis
89   - verify(devicePropertyMapper, never()).insert(any(), any(), anyLong());
  89 + verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong());
90 90 verify(deviceDataRedisDAO, never()).putAll(anyLong(), any());
91 91 }
92 92  
... ... @@ -187,7 +187,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest {
187 187 @SuppressWarnings("unchecked")
188 188 private Map<String, Object> captureMapperInsertProperties() {
189 189 ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class);
190   - verify(devicePropertyMapper).insert(any(IotDeviceDO.class), captor.capture(), anyLong());
  190 + verify(devicePropertyMapper).insert(any(IotDeviceDO.class), captor.capture(), anyLong(), anyLong());
191 191 return captor.getValue();
192 192 }
193 193  
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotDatabaseDataRuleActionTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action;
  2 +
  3 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
  4 +import com.zaxxer.hikari.HikariDataSource;
  5 +import org.junit.jupiter.api.BeforeEach;
  6 +import org.junit.jupiter.api.Test;
  7 +import org.mockito.Mock;
  8 +import org.mockito.MockitoAnnotations;
  9 +import org.springframework.jdbc.core.JdbcTemplate;
  10 +
  11 +import javax.sql.DataSource;
  12 +
  13 +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
  14 +import static org.junit.jupiter.api.Assertions.assertEquals;
  15 +import static org.mockito.Mockito.*;
  16 +
  17 +/**
  18 + * {@link IotDatabaseDataRuleAction} 的单元测试
  19 + *
  20 + * @author HUIHUI
  21 + */
  22 +class IotDatabaseDataRuleActionTest {
  23 +
  24 + private IotDatabaseDataRuleAction databaseDataRuleAction;
  25 +
  26 + @Mock
  27 + private JdbcTemplate jdbcTemplate;
  28 +
  29 + @BeforeEach
  30 + public void setUp() {
  31 + MockitoAnnotations.openMocks(this);
  32 + databaseDataRuleAction = new IotDatabaseDataRuleAction();
  33 + }
  34 +
  35 + @Test
  36 + public void testGetType() {
  37 + // 调用 & 断言:返回 Database 类型枚举值
  38 + assertEquals(IotDataSinkTypeEnum.DATABASE.getType(), databaseDataRuleAction.getType());
  39 + }
  40 +
  41 + @Test
  42 + public void testCloseProducer_whenHikari() throws Exception {
  43 + // 准备:底层是 HikariDataSource
  44 + HikariDataSource hikari = mock(HikariDataSource.class);
  45 + when(jdbcTemplate.getDataSource()).thenReturn(hikari);
  46 +
  47 + // 调用
  48 + databaseDataRuleAction.closeProducer(jdbcTemplate);
  49 +
  50 + // 断言:HikariDataSource 被关闭
  51 + verify(hikari, times(1)).close();
  52 + }
  53 +
  54 + @Test
  55 + public void testCloseProducer_whenNotHikari() throws Exception {
  56 + // 准备:底层不是 HikariDataSource,避免误调 close
  57 + DataSource other = mock(DataSource.class);
  58 + when(jdbcTemplate.getDataSource()).thenReturn(other);
  59 +
  60 + // 调用 & 断言:不抛异常,且不会尝试关闭非 Hikari 数据源
  61 + assertDoesNotThrow(() -> databaseDataRuleAction.closeProducer(jdbcTemplate));
  62 + verifyNoInteractions(other);
  63 + }
  64 +
  65 +}
... ...
yudao-module-iot/yudao-module-iot-biz/src/test/java/cn/iocoder/yudao/module/iot/service/rule/data/action/IotMqttDataRuleActionTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.iot.service.rule.data.action;
  2 +
  3 +import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
  4 +import org.eclipse.paho.client.mqttv3.MqttClient;
  5 +import org.junit.jupiter.api.BeforeEach;
  6 +import org.junit.jupiter.api.Test;
  7 +import org.mockito.Mock;
  8 +import org.mockito.MockitoAnnotations;
  9 +
  10 +import static org.junit.jupiter.api.Assertions.assertEquals;
  11 +import static org.mockito.Mockito.*;
  12 +
  13 +/**
  14 + * {@link IotMqttDataRuleAction} 的单元测试
  15 + *
  16 + * @author HUIHUI
  17 + */
  18 +class IotMqttDataRuleActionTest {
  19 +
  20 + private IotMqttDataRuleAction mqttDataRuleAction;
  21 +
  22 + @Mock
  23 + private MqttClient mqttClient;
  24 +
  25 + @BeforeEach
  26 + public void setUp() {
  27 + MockitoAnnotations.openMocks(this);
  28 + mqttDataRuleAction = new IotMqttDataRuleAction();
  29 + }
  30 +
  31 + @Test
  32 + public void testGetType() {
  33 + // 调用 & 断言:返回 MQTT 类型枚举值
  34 + assertEquals(IotDataSinkTypeEnum.MQTT.getType(), mqttDataRuleAction.getType());
  35 + }
  36 +
  37 + @Test
  38 + public void testCloseProducer_whenConnected() throws Exception {
  39 + // 准备:连接中状态
  40 + when(mqttClient.isConnected()).thenReturn(true);
  41 +
  42 + // 调用
  43 + mqttDataRuleAction.closeProducer(mqttClient);
  44 +
  45 + // 断言:先 disconnect 再 close
  46 + verify(mqttClient, times(1)).disconnect();
  47 + verify(mqttClient, times(1)).close();
  48 + }
  49 +
  50 + @Test
  51 + public void testCloseProducer_whenAlreadyDisconnected() throws Exception {
  52 + // 准备:已断开状态
  53 + when(mqttClient.isConnected()).thenReturn(false);
  54 +
  55 + // 调用
  56 + mqttDataRuleAction.closeProducer(mqttClient);
  57 +
  58 + // 断言:跳过 disconnect,仅 close
  59 + verify(mqttClient, never()).disconnect();
  60 + verify(mqttClient, times(1)).close();
  61 + }
  62 +
  63 +}
... ...