Commit e89724e5cdcfc12a45bf565909d7f183ad7b9ed5

Authored by YunaiV
1 parent f495f9c8

✨ feat(im): 修复单测报错的问题

yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImPrivateMessageMapperTest.java
@@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; @@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
5 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 5 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
6 import jakarta.annotation.Resource; 6 import jakarta.annotation.Resource;
7 import org.junit.jupiter.api.Test; 7 import org.junit.jupiter.api.Test;
8 -import org.springframework.context.annotation.Import;  
9 8
10 import java.time.LocalDateTime; 9 import java.time.LocalDateTime;
11 import java.util.List; 10 import java.util.List;
@@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*; @@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.*;
17 * 16 *
18 * @author 芋道源码 17 * @author 芋道源码
19 */ 18 */
20 -@Import(ImPrivateMessageMapper.class)  
21 public class ImPrivateMessageMapperTest extends BaseDbUnitTest { 19 public class ImPrivateMessageMapperTest extends BaseDbUnitTest {
22 20
23 @Resource 21 @Resource
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
1 package cn.iocoder.yudao.module.im.service.message; 1 package cn.iocoder.yudao.module.im.service.message;
2 2
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
3 import cn.iocoder.yudao.framework.common.exception.ServiceException; 4 import cn.iocoder.yudao.framework.common.exception.ServiceException;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
4 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO;
5 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO;
6 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; 9 import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO;
7 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; 10 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO;
8 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; 11 import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper;
9 import cn.iocoder.yudao.module.im.dal.redis.group.GroupReadPositionRedisDAO; 12 import cn.iocoder.yudao.module.im.dal.redis.group.GroupReadPositionRedisDAO;
10 -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;  
11 import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; 13 import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum;
12 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 14 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
13 -import cn.iocoder.yudao.module.im.service.group.ImGroupService;  
14 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; 15 import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService;
  16 +import cn.iocoder.yudao.module.im.service.group.ImGroupService;
15 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 17 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
16 -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;  
17 import org.junit.jupiter.api.Test; 18 import org.junit.jupiter.api.Test;
18 -import org.junit.jupiter.api.extension.ExtendWith;  
19 import org.mockito.InjectMocks; 19 import org.mockito.InjectMocks;
20 import org.mockito.Mock; 20 import org.mockito.Mock;
21 -import org.mockito.junit.jupiter.MockitoExtension;  
22 21
23 import java.time.LocalDateTime; 22 import java.time.LocalDateTime;
24 import java.util.*; 23 import java.util.*;
@@ -28,14 +27,12 @@ import static org.junit.jupiter.api.Assertions.*; @@ -28,14 +27,12 @@ import static org.junit.jupiter.api.Assertions.*;
28 import static org.mockito.ArgumentMatchers.*; 27 import static org.mockito.ArgumentMatchers.*;
29 import static org.mockito.Mockito.*; 28 import static org.mockito.Mockito.*;
30 29
31 -// DONE @芋艿:已评估,23 个测试全部通过  
32 /** 30 /**
33 * IM 群聊消息 Service 单元测试 31 * IM 群聊消息 Service 单元测试
34 * 32 *
35 * @author 芋道源码 33 * @author 芋道源码
36 */ 34 */
37 -@ExtendWith(MockitoExtension.class)  
38 -public class ImGroupMessageServiceImplTest { 35 +public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest {
39 36
40 @InjectMocks 37 @InjectMocks
41 private ImGroupMessageServiceImpl groupMessageService; 38 private ImGroupMessageServiceImpl groupMessageService;
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
@@ -2,19 +2,18 @@ package cn.iocoder.yudao.module.im.service.message; @@ -2,19 +2,18 @@ package cn.iocoder.yudao.module.im.service.message;
2 2
3 import cn.hutool.extra.spring.SpringUtil; 3 import cn.hutool.extra.spring.SpringUtil;
4 import cn.iocoder.yudao.framework.common.exception.ServiceException; 4 import cn.iocoder.yudao.framework.common.exception.ServiceException;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
5 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 7 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
6 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 8 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
7 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 9 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
8 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 10 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
9 import cn.iocoder.yudao.module.im.service.friend.ImFriendService; 11 import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
10 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; 12 import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
11 -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;  
12 import org.junit.jupiter.api.Test; 13 import org.junit.jupiter.api.Test;
13 -import org.junit.jupiter.api.extension.ExtendWith;  
14 import org.mockito.InjectMocks; 14 import org.mockito.InjectMocks;
15 import org.mockito.Mock; 15 import org.mockito.Mock;
16 import org.mockito.MockedStatic; 16 import org.mockito.MockedStatic;
17 -import org.mockito.junit.jupiter.MockitoExtension;  
18 17
19 import java.time.LocalDateTime; 18 import java.time.LocalDateTime;
20 import java.util.List; 19 import java.util.List;
@@ -29,8 +28,7 @@ import static org.mockito.Mockito.*; @@ -29,8 +28,7 @@ import static org.mockito.Mockito.*;
29 * 28 *
30 * @author 芋道源码 29 * @author 芋道源码
31 */ 30 */
32 -@ExtendWith(MockitoExtension.class)  
33 -public class ImPrivateMessageServiceImplTest { 31 +public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest {
34 32
35 @InjectMocks 33 @InjectMocks
36 private ImPrivateMessageServiceImpl privateMessageService; 34 private ImPrivateMessageServiceImpl privateMessageService;
@@ -155,22 +153,28 @@ public class ImPrivateMessageServiceImplTest { @@ -155,22 +153,28 @@ public class ImPrivateMessageServiceImplTest {
155 153
156 @Test 154 @Test
157 public void testReadMessages_success() { 155 public void testReadMessages_success() {
158 - // 准备:mock 未读消息列表  
159 - List<ImPrivateMessageDO> unreadMessages = List.of(  
160 - ImPrivateMessageDO.builder().id(1L).senderId(2L).receiverId(1L)  
161 - .status(ImMessageStatusEnum.UNREAD.getStatus()).build(),  
162 - ImPrivateMessageDO.builder().id(2L).senderId(2L).receiverId(1L)  
163 - .status(ImMessageStatusEnum.UNREAD.getStatus()).build()  
164 - );  
165 - when(imPrivateMessageMapper.selectListBySenderIdAndReceiverIdAndStatus(2L, 1L,  
166 - ImMessageStatusEnum.UNREAD.getStatus())).thenReturn(unreadMessages); 156 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  157 + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class)))
  158 + .thenReturn(privateMessageService);
167 159
168 - // 调用  
169 - privateMessageService.readPrivateMessages(1L, 2L); 160 + // 准备:mock 未读消息列表
  161 + List<ImPrivateMessageDO> unreadMessages = List.of(
  162 + ImPrivateMessageDO.builder().id(1L).senderId(2L).receiverId(1L)
  163 + .status(ImMessageStatusEnum.UNREAD.getStatus()).build(),
  164 + ImPrivateMessageDO.builder().id(2L).senderId(2L).receiverId(1L)
  165 + .status(ImMessageStatusEnum.UNREAD.getStatus()).build()
  166 + );
  167 + when(imPrivateMessageMapper.selectListBySenderIdAndReceiverIdAndStatus(2L, 1L,
  168 + ImMessageStatusEnum.UNREAD.getStatus())).thenReturn(unreadMessages);
170 169
171 - // 断言:更新了消息状态 + 发送了 READ 和 RECEIPT 事件  
172 - verify(imPrivateMessageMapper).update(any(ImPrivateMessageDO.class), any());  
173 - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); 170 + // 调用
  171 + privateMessageService.readPrivateMessages(1L, 2L);
  172 +
  173 + // 断言:更新了消息状态 + 发送了 READ 和 RECEIPT 事件
  174 + verify(imPrivateMessageMapper).updateByIdsAndStatus(eq(List.of(1L, 2L)),
  175 + eq(ImMessageStatusEnum.UNREAD.getStatus()), any(ImPrivateMessageDO.class));
  176 + verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any());
  177 + }
174 } 178 }
175 179
176 // ========== 撤回测试 ========== 180 // ========== 撤回测试 ==========
@@ -208,7 +212,8 @@ public class ImPrivateMessageServiceImplTest { @@ -208,7 +212,8 @@ public class ImPrivateMessageServiceImplTest {
208 // 准备 212 // 准备
209 ImPrivateMessageDO message = ImPrivateMessageDO.builder() 213 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
210 .id(10L).senderId(2L).receiverId(1L) 214 .id(10L).senderId(2L).receiverId(1L)
211 - .status(ImMessageStatusEnum.UNREAD.getStatus()).build(); 215 + .status(ImMessageStatusEnum.UNREAD.getStatus())
  216 + .sendTime(LocalDateTime.now()).build();
212 when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); 217 when(imPrivateMessageMapper.selectById(10L)).thenReturn(message);
213 218
214 // 调用并断言 219 // 调用并断言
@@ -222,7 +227,8 @@ public class ImPrivateMessageServiceImplTest { @@ -222,7 +227,8 @@ public class ImPrivateMessageServiceImplTest {
222 // 准备 227 // 准备
223 ImPrivateMessageDO message = ImPrivateMessageDO.builder() 228 ImPrivateMessageDO message = ImPrivateMessageDO.builder()
224 .id(10L).senderId(1L).receiverId(2L) 229 .id(10L).senderId(1L).receiverId(2L)
225 - .status(ImMessageStatusEnum.RECALL.getStatus()).build(); 230 + .status(ImMessageStatusEnum.RECALL.getStatus())
  231 + .sendTime(LocalDateTime.now()).build();
226 when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); 232 when(imPrivateMessageMapper.selectById(10L)).thenReturn(message);
227 233
228 // 调用并断言 234 // 调用并断言
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java
1 package cn.iocoder.yudao.module.im.service.message; 1 package cn.iocoder.yudao.module.im.service.message;
2 2
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
3 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; 5 import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO;
4 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; 6 import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO;
5 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; 7 import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper;
6 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; 8 import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum;
  9 +import cn.iocoder.yudao.module.im.service.friend.ImFriendService;
  10 +import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;
7 import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage; 11 import cn.iocoder.yudao.module.im.websocket.ImMessageReadMessage;
8 import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage; 12 import cn.iocoder.yudao.module.im.websocket.ImMessageReceiptMessage;
9 -import cn.iocoder.yudao.module.im.websocket.ImMessageRecallMessage;  
10 import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage; 13 import cn.iocoder.yudao.module.im.websocket.ImPrivateMessageSendMessage;
11 -import cn.iocoder.yudao.module.im.service.friend.ImFriendService;  
12 -import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService;  
13 -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;  
14 import org.junit.jupiter.api.Test; 14 import org.junit.jupiter.api.Test;
15 -import org.junit.jupiter.api.extension.ExtendWith;  
16 import org.mockito.ArgumentCaptor; 15 import org.mockito.ArgumentCaptor;
17 import org.mockito.InjectMocks; 16 import org.mockito.InjectMocks;
18 import org.mockito.Mock; 17 import org.mockito.Mock;
19 -import org.mockito.junit.jupiter.MockitoExtension;  
20 18
21 import java.util.List; 19 import java.util.List;
22 20
23 -import static org.junit.jupiter.api.Assertions.*; 21 +import static org.junit.jupiter.api.Assertions.assertEquals;
  22 +import static org.junit.jupiter.api.Assertions.assertNotNull;
24 import static org.mockito.ArgumentMatchers.*; 23 import static org.mockito.ArgumentMatchers.*;
25 import static org.mockito.Mockito.*; 24 import static org.mockito.Mockito.*;
26 25
@@ -31,8 +30,7 @@ import static org.mockito.Mockito.*; @@ -31,8 +30,7 @@ import static org.mockito.Mockito.*;
31 * 30 *
32 * @author 芋道源码 31 * @author 芋道源码
33 */ 32 */
34 -@ExtendWith(MockitoExtension.class)  
35 -public class ImWebSocketEventTest { 33 +public class ImWebSocketEventTest extends BaseMockitoUnitTest {
36 34
37 @InjectMocks 35 @InjectMocks
38 private ImPrivateMessageServiceImpl privateMessageService; 36 private ImPrivateMessageServiceImpl privateMessageService;
@@ -48,92 +46,104 @@ public class ImWebSocketEventTest { @@ -48,92 +46,104 @@ public class ImWebSocketEventTest {
48 46
49 @Test 47 @Test
50 public void testSendMessage_websocketEvent() { 48 public void testSendMessage_websocketEvent() {
51 - // 准备  
52 - ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO();  
53 - reqVO.setClientMessageId("evt-uuid");  
54 - reqVO.setReceiverId(2L);  
55 - reqVO.setType(0);  
56 - reqVO.setContent("{\"content\":\"事件测试\"}");  
57 -  
58 - when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "evt-uuid")).thenReturn(null);  
59 - // mock insert:通过 doAnswer 模拟 MyBatis-Plus 设置 ID  
60 - when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenAnswer(invocation -> {  
61 - ImPrivateMessageDO msg = invocation.getArgument(0);  
62 - msg.setId(99L);  
63 - return 1;  
64 - });  
65 -  
66 - // 调用  
67 - privateMessageService.sendPrivateMessage(1L, reqVO);  
68 -  
69 - // 捕获 WebSocket 调用  
70 - ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);  
71 - ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class);  
72 - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(),  
73 - typeCaptor.capture(), contentCaptor.capture());  
74 -  
75 - // 验证消息事件类型  
76 - assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(0));  
77 - assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(1));  
78 -  
79 - // 验证 payload 是 DTO 类型  
80 - ImPrivateMessageSendMessage payload = (ImPrivateMessageSendMessage) contentCaptor.getAllValues().get(0);  
81 - assertNotNull(payload.getId(), "id 不应为空");  
82 - assertEquals("1", payload.getSenderId());  
83 - assertEquals("2", payload.getReceiverId()); 49 + try (var springUtilMockedStatic = mockStatic(cn.hutool.extra.spring.SpringUtil.class)) {
  50 + springUtilMockedStatic.when(() -> cn.hutool.extra.spring.SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class)))
  51 + .thenReturn(privateMessageService);
  52 +
  53 + // 准备
  54 + ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO();
  55 + reqVO.setClientMessageId("evt-uuid");
  56 + reqVO.setReceiverId(2L);
  57 + reqVO.setType(0);
  58 + reqVO.setContent("{\"content\":\"事件测试\"}");
  59 +
  60 + when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "evt-uuid")).thenReturn(null);
  61 + when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenAnswer(invocation -> {
  62 + ImPrivateMessageDO msg = invocation.getArgument(0);
  63 + msg.setId(99L);
  64 + return 1;
  65 + });
  66 +
  67 + // 调用
  68 + privateMessageService.sendPrivateMessage(1L, reqVO);
  69 +
  70 + // 捕获 WebSocket 调用
  71 + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
  72 + ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class);
  73 + verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(),
  74 + typeCaptor.capture(), contentCaptor.capture());
  75 +
  76 + // 验证消息事件类型
  77 + assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(0));
  78 + assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(1));
  79 +
  80 + // 验证 payload 是 DTO 类型
  81 + ImPrivateMessageSendMessage payload = (ImPrivateMessageSendMessage) contentCaptor.getAllValues().get(0);
  82 + assertNotNull(payload.getId(), "id 不应为空");
  83 + assertEquals(1L, payload.getSenderId());
  84 + assertEquals(2L, payload.getReceiverId());
  85 + }
84 } 86 }
85 87
86 @Test 88 @Test
87 public void testReadMessage_readAndReceiptEvents() { 89 public void testReadMessage_readAndReceiptEvents() {
88 - // 准备:mock 未读消息  
89 - List<ImPrivateMessageDO> unreadMessages = List.of(  
90 - ImPrivateMessageDO.builder().id(1L).senderId(2L).receiverId(1L)  
91 - .status(ImMessageStatusEnum.UNREAD.getStatus()).build()  
92 - );  
93 - when(imPrivateMessageMapper.selectListBySenderIdAndReceiverIdAndStatus(2L, 1L,  
94 - ImMessageStatusEnum.UNREAD.getStatus())).thenReturn(unreadMessages);  
95 -  
96 - // 调用  
97 - privateMessageService.readPrivateMessages(1L, 2L);  
98 -  
99 - // 捕获  
100 - ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);  
101 - ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class);  
102 - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), userCaptor.capture(),  
103 - typeCaptor.capture(), any());  
104 -  
105 - // 第一次:发给自己的 READ 事件  
106 - assertEquals(ImMessageReadMessage.TYPE, typeCaptor.getAllValues().get(0));  
107 - assertEquals(1L, userCaptor.getAllValues().get(0));  
108 - // 第二次:发给对方的 RECEIPT 事件  
109 - assertEquals(ImMessageReceiptMessage.TYPE, typeCaptor.getAllValues().get(1));  
110 - assertEquals(2L, userCaptor.getAllValues().get(1)); 90 + try (var springUtilMockedStatic = mockStatic(cn.hutool.extra.spring.SpringUtil.class)) {
  91 + springUtilMockedStatic.when(() -> cn.hutool.extra.spring.SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class)))
  92 + .thenReturn(privateMessageService);
  93 +
  94 + // 准备:mock 未读消息
  95 + List<ImPrivateMessageDO> unreadMessages = List.of(
  96 + ImPrivateMessageDO.builder().id(1L).senderId(2L).receiverId(1L)
  97 + .status(ImMessageStatusEnum.UNREAD.getStatus()).build()
  98 + );
  99 + when(imPrivateMessageMapper.selectListBySenderIdAndReceiverIdAndStatus(2L, 1L,
  100 + ImMessageStatusEnum.UNREAD.getStatus())).thenReturn(unreadMessages);
  101 +
  102 + // 调用
  103 + privateMessageService.readPrivateMessages(1L, 2L);
  104 +
  105 + // 捕获
  106 + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
  107 + ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class);
  108 + verify(webSocketMessageSender, times(2)).sendObject(anyInt(), userCaptor.capture(),
  109 + typeCaptor.capture(), any());
  110 +
  111 + // 第一次:发给自己的 READ 事件
  112 + assertEquals(ImMessageReadMessage.TYPE, typeCaptor.getAllValues().get(0));
  113 + assertEquals(1L, userCaptor.getAllValues().get(0));
  114 + // 第二次:发给对方的 RECEIPT 事件
  115 + assertEquals(ImMessageReceiptMessage.TYPE, typeCaptor.getAllValues().get(1));
  116 + assertEquals(2L, userCaptor.getAllValues().get(1));
  117 + }
111 } 118 }
112 119
113 @Test 120 @Test
114 - public void testRecallMessage_recallEvent() {  
115 - // 准备  
116 - ImPrivateMessageDO message = ImPrivateMessageDO.builder()  
117 - .id(10L).senderId(1L).receiverId(2L).status(0).build();  
118 - when(imPrivateMessageMapper.selectById(10L)).thenReturn(message);  
119 - when(imPrivateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1);  
120 -  
121 - // 调用  
122 - privateMessageService.recallPrivateMessage(1L, 10L);  
123 -  
124 - // 捕获  
125 - ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);  
126 - ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class);  
127 - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(),  
128 - typeCaptor.capture(), contentCaptor.capture());  
129 -  
130 - // 验证 RECALL 事件类型  
131 - assertEquals(ImMessageRecallMessage.TYPE, typeCaptor.getAllValues().get(0));  
132 -  
133 - // 验证 payload  
134 - ImMessageRecallMessage payload = (ImMessageRecallMessage) contentCaptor.getAllValues().get(0);  
135 - assertEquals("10", payload.getMessageId());  
136 - assertEquals("private", payload.getMessageScene()); 121 + public void testRecallMessage_tipTextEvent() {
  122 + try (var springUtilMockedStatic = mockStatic(cn.hutool.extra.spring.SpringUtil.class)) {
  123 + springUtilMockedStatic.when(() -> cn.hutool.extra.spring.SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class)))
  124 + .thenReturn(privateMessageService);
  125 +
  126 + // 准备
  127 + ImPrivateMessageDO message = ImPrivateMessageDO.builder()
  128 + .id(10L).senderId(1L).receiverId(2L).status(0)
  129 + .sendTime(java.time.LocalDateTime.now()).build();
  130 + when(imPrivateMessageMapper.selectById(10L)).thenReturn(message);
  131 + when(imPrivateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1);
  132 + when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenReturn(1);
  133 +
  134 + // 调用
  135 + privateMessageService.recallPrivateMessage(1L, 10L);
  136 +
  137 + // 捕获
  138 + ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class);
  139 + ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class);
  140 + verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(),
  141 + typeCaptor.capture(), contentCaptor.capture());
  142 +
  143 + // 验证:推送的是 TIP_TEXT 消息(通过 sendPrivateMessageEvent)
  144 + assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(0));
  145 + assertEquals(ImPrivateMessageSendMessage.TYPE, typeCaptor.getAllValues().get(1));
  146 + }
137 } 147 }
138 148
139 } 149 }
yudao-module-im/yudao-module-im-biz/src/test/resources/application-unit-test.yaml
1 spring: 1 spring:
2 main: 2 main:
3 - lazy-initialization: true  
4 - banner-mode: off 3 + lazy-initialization: true # 开启懒加载,加快速度
  4 + banner-mode: off # 单元测试,禁用 Banner
5 5
6 --- #################### 数据库相关配置 #################### 6 --- #################### 数据库相关配置 ####################
7 7
8 spring: 8 spring:
  9 + # 数据源配置项
9 datasource: 10 datasource:
10 name: ruoyi-vue-pro 11 name: ruoyi-vue-pro
11 - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; 12 + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
12 driver-class-name: org.h2.Driver 13 driver-class-name: org.h2.Driver
13 username: sa 14 username: sa
14 password: 15 password:
15 druid: 16 druid:
16 - async-init: true  
17 - initial-size: 1 17 + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度
  18 + initial-size: 1 # 单元测试,配置为 1,提升启动速度
18 sql: 19 sql:
19 init: 20 init:
20 schema-locations: classpath:/sql/create_tables.sql 21 schema-locations: classpath:/sql/create_tables.sql
21 encoding: UTF-8 22 encoding: UTF-8
22 23
  24 + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
23 data: 25 data:
24 redis: 26 redis:
25 - host: 127.0.0.1  
26 - port: 16379  
27 - database: 0 27 + host: 127.0.0.1 # 地址
  28 + port: 16379 # 端口(单元测试,使用 16379 端口)
  29 + database: 0 # 数据库索引
28 30
29 -mybatis-plus:  
30 - lazy-initialization: true  
31 - type-aliases-package: ${yudao.info.base-package}.module.*.dal.dataobject 31 +mybatis:
  32 + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试
  33 +
  34 +--- #################### 定时任务相关配置 ####################
  35 +
  36 +--- #################### 配置中心相关配置 ####################
  37 +
  38 +--- #################### 服务保障相关配置 ####################
  39 +
  40 +# Lock4j 配置项(单元测试,禁用 Lock4j)
  41 +
  42 +--- #################### 监控相关配置 ####################
32 43
33 --- #################### 芋道相关配置 #################### 44 --- #################### 芋道相关配置 ####################
34 45
  46 +# 芋道配置项,设置当前项目所有自定义的配置
35 yudao: 47 yudao:
36 info: 48 info:
37 - base-package: cn.iocoder.yudao 49 + base-package: cn.iocoder.yudao.module