Websocket 的单元测试返回 Null

Posted

技术标签:

【中文标题】Websocket 的单元测试返回 Null【英文标题】:Unit Test For Websocket Returns Null 【发布时间】:2020-11-02 07:39:30 【问题描述】:

我正在尝试使用JUnitSpringboot 中的Websocket 实施单元测试。

我正在关注this 链接,它运行良好,但结果为空,当我使用浏览器测试我的 websocket 时,它会发送所需的结果。

让我展示我的测试类

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes =Main.class)
@WebAppConfiguration
public class WebSocketTest 

static final String WEBSOCKET_URI = "http://localhost:8080/api/realtimedata";
static final String WEBSOCKET_TOPIC = "/realtimedata";

BlockingQueue<WebSocketData> blockingQueue;
WebSocketStompClient stompClient;

@BeforeEach
public void setup() 

    blockingQueue = new LinkedBlockingDeque<>();
    stompClient = new WebSocketStompClient(new SockJsClient(
            asList(new WebSocketTransport(new StandardWebSocketClient()))));


@Test
public void shouldReceiveAMessageFromTheServer() throws Exception 
    StompSession session = stompClient
            .connect(WEBSOCKET_URI, new StompSessionHandlerAdapter() )
            .get(1, SECONDS);
    session.subscribe(WEBSOCKET_TOPIC, new DefaultStompFrameHandler());

    String message = "\"groupId\":\"grp-1\", \"accountId\":\"acc-1\", \"userId\":\"usr-1\"";
    session.send(WEBSOCKET_TOPIC, message.getBytes()); 
    Assertions.assertEquals("trying to figureout", blockingQueue.poll(1, SECONDS));


class DefaultStompFrameHandler implements StompFrameHandler 
    @Override
    public Type getPayloadType(StompHeaders stompHeaders) 
        return WebSocketData.class;
    

    @Override
    public void handleFrame(StompHeaders stompHeaders, Object o) 
        blockingQueue.offer((WebSocketData) o);
    

这是我的WebSocketConfiguration 课程

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer 

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) 
    registry.enableSimpleBroker("/realtimedata");
    registry.setApplicationDestinationPrefixes("/wsconn");


@Override
public void registerStompEndpoints(StompEndpointRegistry registry) 
    registry.addEndpoint("/realtimedata")
            .setAllowedOrigins("*")
            .withSockJS();

这是我从服务器得到的响应

org.opentest4j.AssertionFailedError: 
Expected :trying to figureout
Actual   :null

更新:

在下面的答案中添加Rieckpil 推荐的更改并经过进一步研究后,我的测试类现在看起来像这样

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ServerWebSocketTest 

@LocalServerPort
private Integer port;

static final String WEBSOCKET_TOPIC = "/realtimedata";

BlockingQueue<WebSocketData> blockingQueue;
WebSocketStompClient stompClient;

@BeforeEach
public void setup() 

    blockingQueue = new LinkedBlockingDeque<>();
    stompClient = new WebSocketStompClient(new SockJsClient(
            asList(new WebSocketTransport(new StandardWebSocketClient()))));

    // might be required
    stompClient.setMessageConverter(new MappingJackson2MessageConverter());



@Test
public void shouldReceiveAMessageFromTheServer() throws Exception 
    StompSession session = stompClient
            .connect(getWsPath(), **new DefaultStompFrameHandler()** 
            )
            .get(1, TimeUnit.SECONDS);
    session.subscribe(WEBSOCKET_TOPIC, new DefaultStompFrameHandler());

    String message = "\"groupId\":\"grp-1\", \"accountId\":\"acc-1\", \"userId\":\"usr-1\"";
    session.send(WEBSOCKET_TOPIC, message.getBytes());
    Assertions.assertEquals("trying to figureout", blockingQueue.poll(1, TimeUnit.SECONDS));


class DefaultStompFrameHandler **extends StompSessionHandlerAdapter**   
    @Override
    public Type getPayloadType(StompHeaders stompHeaders) 
        return WebSocketData.class;
    

    @Override
    public void handleFrame(StompHeaders stompHeaders, Object o) 
        blockingQueue.offer((WebSocketData) o);
    

    **@Override
    public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) 
        exception.printStackTrace();
    **

private String getWsPath() 
    return String.format("ws://localhost:%d/location_services/realtimedata", port);

在这些更改之后,我现在可以在日志中打印堆栈跟踪

这是我在这些更改后得到的例外

 org.springframework.messaging.converter.MessageConversionException: Could not read JSON: Cannot construct instance of `com.convo.locationservices.websocket.WebSocketData` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('eyJncm91cElkIjoiZ3JwLTEiLCAiYWNjb3VudElkIjoiYWNjLTEiLCAidXNlcklkIjoidXNyLTEifQ==')
 at [Source: (byte[])""eyJncm91cElkIjoiZ3JwLTEiLCAiYWNjb3VudElkIjoiYWNjLTEiLCAidXNlcklkIjoidXNyLTEifQ==""; line: 1, column: 1]; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.convo.locationservices.websocket.WebSocketData` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('eyJncm91cElkIjoiZ3JwLTEiLCAiYWNjb3VudElkIjoiYWNjLTEiLCAidXNlcklkIjoidXNyLTEifQ==')
 at [Source: (byte[])""eyJncm91cElkIjoiZ3JwLTEiLCAiYWNjb3VudElkIjoiYWNjLTEiLCAidXNlcklkIjoidXNyLTEifQ==""; line: 1, column: 1], failedMessage=GenericMessage [payload=byte[82], headers=simpMessageType=MESSAGE, stompCommand=MESSAGE, nativeHeaders=destination=[/realtimedata], content-type=[application/json], subscription=[0], message-id=[cc55448f0b4f447d8f6bc7662acd15db-0], content-length=[82], simpSubscriptionId=0, contentType=application/json, simpSessionId=2df73ca0-e13b-c479-4591-37e9f7e8d689, simpDestination=/realtimedata]
    at org.springframework.messaging.converter.MappingJackson2MessageConverter.convertFromInternal(MappingJackson2MessageConverter.java:235)
    at org.springframework.messaging.converter.AbstractMessageConverter.fromMessage(AbstractMessageConverter.java:197)
    at org.springframework.messaging.converter.AbstractMessageConverter.fromMessage(AbstractMessageConverter.java:188)
    at org.springframework.messaging.simp.stomp.DefaultStompSession.invokeHandler(DefaultStompSession.java:477)
    at org.springframework.messaging.simp.stomp.DefaultStompSession.handleMessage(DefaultStompSession.java:429)
    at org.springframework.web.socket.messaging.WebSocketStompClient$WebSocketTcpConnectionHandlerAdapter.handleMessage(WebSocketStompClient.java:340)
    at org.springframework.web.socket.handler.WebSocketHandlerDecorator.handleMessage(WebSocketHandlerDecorator.java:75)
    at org.springframework.web.socket.handler.LoggingWebSocketHandlerDecorator.handleMessage(LoggingWebSocketHandlerDecorator.java:56)
    at org.springframework.web.socket.sockjs.client.AbstractClientSockJsSession.handleMessageFrame(AbstractClientSockJsSession.java:294)
    at org.springframework.web.socket.sockjs.client.AbstractClientSockJsSession.handleFrame(AbstractClientSockJsSession.java:230)
    at org.springframework.web.socket.sockjs.client.WebSocketTransport$ClientSockJsWebSocketHandler.handleTextMessage(WebSocketTransport.java:163)
    at org.springframework.web.socket.handler.AbstractWebSocketHandler.handleMessage(AbstractWebSocketHandler.java:43)
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.handleTextMessage(StandardWebSocketHandlerAdapter.java:114)
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter.access$000(StandardWebSocketHandlerAdapter.java:43)
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:85)
    at org.springframework.web.socket.adapter.standard.StandardWebSocketHandlerAdapter$3.onMessage(StandardWebSocketHandlerAdapter.java:82)
    at org.apache.tomcat.websocket.WsFrameBase.sendMessageText(WsFrameBase.java:395)
    at org.apache.tomcat.websocket.WsFrameBase.processDataText(WsFrameBase.java:495)
    at org.apache.tomcat.websocket.WsFrameBase.processData(WsFrameBase.java:294)
    at org.apache.tomcat.websocket.WsFrameBase.processInputBuffer(WsFrameBase.java:133)
    at org.apache.tomcat.websocket.WsFrameClient.processSocketRead(WsFrameClient.java:95)
    at org.apache.tomcat.websocket.WsFrameClient.resumeProcessing(WsFrameClient.java:209)
    at org.apache.tomcat.websocket.WsFrameClient.access$300(WsFrameClient.java:31)
    at org.apache.tomcat.websocket.WsFrameClient$WsFrameClientCompletionHandler.doResumeProcessing(WsFrameClient.java:186)
    at org.apache.tomcat.websocket.WsFrameClient$WsFrameClientCompletionHandler.completed(WsFrameClient.java:163)
    at org.apache.tomcat.websocket.WsFrameClient$WsFrameClientCompletionHandler.completed(WsFrameClient.java:148)
    at java.base/sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:127)
    at java.base/sun.nio.ch.Invoker$2.run(Invoker.java:219)
    at java.base/sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.convo.locationservices.websocket.WebSocketData` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('eyJncm91cElkIjoiZ3JwLTEiLCAiYWNjb3VudElkIjoiYWNjLTEiLCAidXNlcklkIjoidXNyLTEifQ==')
 at [Source: (byte[])""eyJncm91cElkIjoiZ3JwLTEiLCAiYWNjb3VudElkIjoiYWNjLTEiLCAidXNlcklkIjoidXNyLTEifQ==""; line: 1, column: 1]
    at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63)
    at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1432)
    at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1062)
    at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:371)
    at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:323)
    at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1373)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:171)
    at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:161)
    at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218)
    at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3309)
    at org.springframework.messaging.converter.MappingJackson2MessageConverter.convertFromInternal(MappingJackson2MessageConverter.java:219)
    ... 31 more
            

org.opentest4j.AssertionFailedError: 
Expected :trying to figureout
Actual   :null
 

谁能指导我我做错了什么?

谢谢。

【问题讨论】:

你能添加你的 WebSocket 配置吗?另外,您确定您实际上连接到加载的上下文,而不是连接到在端口 8080 上并行运行的应用程序吗? @rieckpil 感谢您的回复,我已经添加了 websocket 配置类。如果我停止在端口 8080 上运行 websocket 的应用程序,则此单元测试会给我异常.....“org.springframework.web.client.ResourceAccessException:对“localhost:8080/api/realtimedata/info”的 GET 请求出现 I/O 错误:连接被拒绝(连接被拒绝);嵌套异常是 java.net.ConnectException:连接被拒绝(连接被拒绝)”....“原因:java.net.ConnectException:连接被拒绝(连接被拒绝)” 我已经更新了我的答案,试试ByteArrayMessageConverter。如果这不起作用,还请包括您接受传入有效负载的服务器端 【参考方案1】:

我也遇到过同样的问题。就我而言,我忘记添加默认构造函数和所有参数构造函数

请检查您的 WebSocketData 类以添加构造函数

【讨论】:

【参考方案2】:

您应该将集成测试配置为使用启动的 Spring 上下文并且不要硬编码 8080

此外,请确保为您的 WebSocketStompClient 配置了适合您的用例的 MessageConverter。默认值为SimpleMessageConverter。当您发送 byte[] 时,ByteArrayMessageConverter 应该适合。

您的测试可以重构为以下内容:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class Test 

  @LocalServerPort
  private Integer port;
  
  static final String WEBSOCKET_TOPIC = "/realtimedata";

  BlockingQueue<WebSocketData> blockingQueue;
  WebSocketStompClient stompClient;

  @BeforeEach
  public void setup() 

    blockingQueue = new LinkedBlockingDeque<>();
    stompClient = new WebSocketStompClient(new SockJsClient(
      asList(new WebSocketTransport(new StandardWebSocketClient()))));

    webSocketStompClient.setMessageConverter(new ByteArrayMessageConverter()); 

  

  @Test
  public void shouldReceiveAMessageFromTheServer() throws Exception 
    StompSession session = stompClient
      .connect(getWsPath(), new StompSessionHandlerAdapter() 
      )
      .get(1, SECONDS);
    session.subscribe(WEBSOCKET_TOPIC, new DefaultStompFrameHandler());

    String message = "\"groupId\":\"grp-1\", \"accountId\":\"acc-1\", \"userId\":\"usr-1\"";
    session.send(WEBSOCKET_TOPIC, message.getBytes());
    Assertions.assertEquals("trying to figureout", blockingQueue.poll(1, SECONDS));
  

  class DefaultStompFrameHandler implements StompFrameHandler 
    @Override
    public Type getPayloadType(StompHeaders stompHeaders) 
      return WebSocketData.class;
    

    @Override
    public void handleFrame(StompHeaders stompHeaders, Object o) 
      blockingQueue.offer((WebSocketData) o);
    
  
  
  private String getWsPath() 
    return String.format("ws://localhost:%d/api/realtimedata", port);
  


这个测试将启动整个 Spring 上下文,随机端口上的嵌入式 servlet 容器,并尝试连接到您的端点。

【讨论】:

感谢您的宝贵时间,非常感谢。但是我对您提到的 MessageConverter 感到困惑,它是否应该将 json(response) 转换为 websocket 返回到客户端应用程序的模型类? 因为我不知道您的生产代码是什么样子(您如何使用有效负载),所以我提前添加了这个注释。首先,尝试不使用任何自定义配置的消息转换器。 无论有没有 MappingJackson2MessageConverter(),响应都是一样的。我进一步研究了这个问题,发现默认情况下不打印异常,为此,我应该覆盖 DefaultStompFrameHandler 中的 handleException() 并且它应该扩展 StompSessionHandlerAdapter 而不是 StompFrameHandler 进行更改后,异常现在显示在日志中 您能否添加现在出现在您的问题中的异常轨迹跟踪? 我在我的问题中添加了新的更改和异常堆栈跟踪。

以上是关于Websocket 的单元测试返回 Null的主要内容,如果未能解决你的问题,请参考以下文章

单元测试基本框架和8个测试方面

Request.GetOwinContext 在单元测试中返回 null - 如何在单元测试中测试 OWIN 身份验证?

为啥 ApplicationsDocumentsDirectory 为单元测试返回 null?

Android 单元测试 LiveData.value 返回 null

使用 OCUnit 进行单元测试时的 NSBundle 返回(空)

编写单元测试用例说明书的依据是啥