Spring Boot STOMP客户端Mock对象验证失败求助
STOMP集成测试中Mockito验证LobbyService方法未被调用排查
我正在开发实时多人游戏API,计划为STOMP API编写集成测试。虽然集成测试里用Mock不算常规操作,但我想验证特定事件下是否调用了正确的端点与方法。
我用@MockBean模拟了LobbyService,但执行verify(lobbyService).matchPlayer(any())时,Mockito提示该方法未被调用。以下是测试代码和调用该方法的控制器代码:
测试代码
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = {"spring.main.allow-bean-definition-overriding=true"}) public class PublishToServerTest { @LocalServerPort private int port; @MockBean private SimpMessagingTemplate messagingTemplate; @MockBean private LobbyService lobbyService; @Test public void testPublishPlayerToLobby() throws InterruptedException { var timeout = 3; // Seconds var webSocketClient = new StandardWebSocketClient(); var stompClient = new WebSocketStompClient(webSocketClient); stompClient.setMessageConverter(new MappingJackson2MessageConverter()); stompClient.setTaskScheduler(new DefaultManagedTaskScheduler()); var sessionHandler = new ExtendedStompSessionHandler() { @Override public void afterConnected(StompSession session, @NonNull StompHeaders connectedHeaders) { var clientPayload = new IncomingPlayerClientPayload("0", 0); session.send("/game/seek-opponent", clientPayload); verify(lobbyService).matchPlayer(any()); getLatch().countDown(); } @Override public void handleException(@NonNull StompSession session, StompCommand command, @NonNull StompHeaders headers, @NonNull byte[] payload, @NonNull Throwable exception) { fail(exception.getMessage()); } }; stompClient.connectAsync(String.format("ws://localhost:%d/ws", port), sessionHandler); var messageSent = sessionHandler.getLatch().await(timeout, TimeUnit.SECONDS); assertTrue(messageSent); } }
控制器代码
@Controller @RequiredArgsConstructor public class LobbyController { private final SimpMessagingTemplate messagingTemplate; private final LobbyService lobbyService; @MessageMapping("/seek-opponent") public void handleIncomingUser(@Payload IncomingPlayer incomingPlayer) { final String notificationUrl = "/lobby/notify"; lobbyService.matchPlayer(incomingPlayer).ifPresentOrElse( multiplayerGame -> { messagingTemplate.convertAndSendToUser( multiplayerGame.getPlayer1().getId(), notificationUrl, new LobbyNotification(multiplayerGame) ); messagingTemplate.convertAndSendToUser( multiplayerGame.getPlayer2().getId(), notificationUrl, new LobbyNotification(multiplayerGame) ); }, () -> messagingTemplate.convertAndSendToUser( incomingPlayer.getId(), notificationUrl, new LobbyNotification() ) ); } }
问题原因与修复方案
1. 验证时机过早
session.send()是异步操作,你在发送消息后立刻调用verify,此时服务器端的handleIncomingUser方法大概率还没执行到matchPlayer。必须等服务器处理完请求后再做验证。
2. 消息类型不匹配
测试里发送的是IncomingPlayerClientPayload,但控制器接收的是IncomingPlayer。如果消息转换器没配置好,无法完成类型转换,控制器方法根本不会被触发,自然不会调用matchPlayer。
3. 线程上下文问题
afterConnected运行在WebSocket客户端线程,和服务器处理请求的线程是分开的,直接在这个回调里做验证可能存在线程可见性问题,或者服务器还没来得及调用Mock方法。
修复后的测试代码
调整验证时机,确保消息类型匹配,并通过CountDownLatch等待服务器处理完成:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestPropertySource(properties = {"spring.main.allow-bean-definition-overriding=true"}) public class PublishToServerTest { @LocalServerPort private int port; @MockBean private SimpMessagingTemplate messagingTemplate; @MockBean private LobbyService lobbyService; private final CountDownLatch serverLatch = new CountDownLatch(1); @Test public void testPublishPlayerToLobby() throws InterruptedException { var timeout = 3; var webSocketClient = new StandardWebSocketClient(); var stompClient = new WebSocketStompClient(webSocketClient); stompClient.setMessageConverter(new MappingJackson2MessageConverter()); stompClient.setTaskScheduler(new DefaultManagedTaskScheduler()); // 提前给Mock设置返回值,避免控制器逻辑卡壳 doReturn(Optional.empty()).when(lobbyService).matchPlayer(any(IncomingPlayer.class)); var sessionHandler = new ExtendedStompSessionHandler() { @Override public void afterConnected(StompSession session, @NonNull StompHeaders connectedHeaders) { // 发送控制器期望的IncomingPlayer类型对象,或者确保转换器能完成转换 var payload = new IncomingPlayer("0", 0); session.send("/game/seek-opponent", payload); // 订阅服务器返回的通知,确认处理完成 session.subscribe("/user/0/lobby/notify", this); } @Override public void handleFrame(StompHeaders headers, Object payload) { // 收到服务器响应后再执行验证 try { verify(lobbyService, times(1)).matchPlayer(any(IncomingPlayer.class)); } finally { serverLatch.countDown(); } } @Override public void handleException(@NonNull StompSession session, StompCommand command, @NonNull StompHeaders headers, @NonNull byte[] payload, @NonNull Throwable exception) { fail(exception.getMessage()); serverLatch.countDown(); } }; stompClient.connectAsync(String.format("ws://localhost:%d/ws", port), sessionHandler); // 等待服务器处理完成 assertTrue(serverLatch.await(timeout, TimeUnit.SECONDS)); // 主线程再做一次验证,确保线程安全 verify(lobbyService, times(1)).matchPlayer(any(IncomingPlayer.class)); } }
额外注意点
- 确保消息转换器能正确处理
IncomingPlayerClientPayload到IncomingPlayer的转换,或者直接发送控制器需要的类型。 - 提前用
doReturn设置Mock的返回值,避免控制器因为Mock返回空而抛出异常。 - 验证操作最好放在主线程或者服务器响应回调中,避免异步时序问题。
内容的提问来源于stack exchange,提问作者Kenuff
相关产品推荐
相关产品推荐

