Commit ac9c11c00f5818cefa2d0cb4e125caac6fa19bd3
1 parent
d668af19
✨ feat(iot): 添加 IotDeviceMessageServiceImpl 的单元测试,增强消息处理的可靠性
Showing
5 changed files
with
216 additions
and
4 deletions
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,11 +97,11 @@ public class IotWebSocketDataRuleAction extends | ||
| 97 | } | 97 | } |
| 98 | } | 98 | } |
| 99 | 99 | ||
| 100 | - // 加锁原因:多线程并发发送消息时,多个线程可能同时检测到连接断开并尝试重连, | ||
| 101 | - // 使用锁 + 双重检查保证同一服务器地址只有一个线程执行重连操作 | ||
| 102 | /** | 100 | /** |
| 103 | * 使用锁进行重连,保证同一服务器地址的重连操作线程安全 | 101 | * 使用锁进行重连,保证同一服务器地址的重连操作线程安全 |
| 104 | * | 102 | * |
| 103 | + * 加锁原因:多线程并发发送消息时,多个线程可能同时检测到连接断开并尝试重连,使用锁 + 双重检查保证同一服务器地址只有一个线程执行重连操作 | ||
| 104 | + * | ||
| 105 | * @param webSocketClient WebSocket 客户端 | 105 | * @param webSocketClient WebSocket 客户端 |
| 106 | * @param config 配置信息 | 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,7 +86,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { | ||
| 86 | service.saveDeviceProperty(device, message); | 86 | service.saveDeviceProperty(device, message); |
| 87 | 87 | ||
| 88 | // 断言:没有合法属性,不会写入 TDengine 与 Redis | 88 | // 断言:没有合法属性,不会写入 TDengine 与 Redis |
| 89 | - verify(devicePropertyMapper, never()).insert(any(), any(), anyLong()); | 89 | + verify(devicePropertyMapper, never()).insert(any(), any(), anyLong(), anyLong()); |
| 90 | verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); | 90 | verify(deviceDataRedisDAO, never()).putAll(anyLong(), any()); |
| 91 | } | 91 | } |
| 92 | 92 | ||
| @@ -187,7 +187,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { | @@ -187,7 +187,7 @@ public class IotDevicePropertyServiceImplTest extends BaseMockitoUnitTest { | ||
| 187 | @SuppressWarnings("unchecked") | 187 | @SuppressWarnings("unchecked") |
| 188 | private Map<String, Object> captureMapperInsertProperties() { | 188 | private Map<String, Object> captureMapperInsertProperties() { |
| 189 | ArgumentCaptor<Map<String, Object>> captor = ArgumentCaptor.forClass(Map.class); | 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 | return captor.getValue(); | 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 | +} |