Developing WebSocket server for your Spring Boot app is fairly simple and well described and documented. However when it comes to making sure that it ‘actually works’ is done manually in most cases.
Below I will show how I do the automated integration tests for Websocket server using Spring’s StompClient. I assume that you are familiar with the idea of WebSockets in Spring. If not, here is a very good article: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html
Source Code
Code of this tutorial is for you to see here: https://github.com/yacekmm/looksok/tree/WebSocketDemo/Spring/WebSocket
System under test: configuration
The demo will be presented on the simpliest WS configuration which consists of one entry point endpoint (`/ws`) and in-memory message broker (under `/queue`):
@Configuration @EnableWebSocketMessageBroker public class WsConfig extends AbstractWebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws"); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/queue"); } }
The idea behind the integration test
In the test I’m going to:
– use SpringRunner to start up the whole application with the full context
– Autowire Component that in production will be responsible for sending messages to WebSocket clients
– Build and configure Spring’s StompClient and connect a StompSession to my WebSocket server
– send a message over WebSocket and verify if my test client received it
Starting the application for tests
With SpringRunner.class used within jUnit test I start the app context and autowire the WSProxy component (the one that sends messages to WS clients):
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = RANDOM_PORT) public class WsConfigIntegrationTest { @Value("${local.server.port}") private int port; @Autowired private WsProxy wsProxy;
WsProxy in this demo is a simple component sending message with a SimpMessagingTemplate:
@Component public class WsProxy { private SimpMessagingTemplate messagingTemplate; @Autowired public WsProxy(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } public void sendMessage(@RequestParam String clientId, @RequestParam String payload){ messagingTemplate.convertAndSend("/queue/" + clientId, payload); } }
In this configuration, the url of WS endpoint is:
String wsUrl = "ws://127.0.0.1:" + port + "/ws";
Configuring StompClient and connecting StompSession
Using the StompClient with a minimum configuration:
WebSocketStompClient stompClient = new WebSocketStompClient(new StandardWebSocketClient()); stompClient.setMessageConverter(new StringMessageConverter());
I create StompSession to my WS url:
StompSession stompSession = stompClient.connect(wsUrl, new MyStompSessionHandler()).get();
The connect() method returns a future, but here, in tests, I wait synchronously until this session is ready by calling get() on it to get the session instantly.
Oh, and don’t worry about the MyStompSessionHandler – in this configuration it does nothing, except debug logging on the ‘Connect to WS’ event (just overrides the StompSessionHandlerAdapter)
Now it’s time to subscribe the /queue/my-id Channel within the session:
stompSession.subscribe( "/queue/my-id", new MyStompFrameHandler((payload) -> resultKeeper.complete(payload.toString())));
The MyStompFrameHandler class is responsible for handling the incoming message in within the session and completing the CompletableFuture promise that it received as an argument. CompletableFuture is a helper variable needed to test asynchronous code:
CompletableFuture<String> resultKeeper = new CompletableFuture<>();
And the handler uses it as follows:
public class MyStompFrameHandler implements StompFrameHandler { private final Consumer<String> frameHandler; public MyStompFrameHandler(Consumer<String> frameHandler) { this.frameHandler = frameHandler; } ... @Override public void handleFrame(StompHeaders headers, Object payload) { log.info("received message: {} with headers: {}", payload, headers); frameHandler.accept(payload.toString()); } }
Sending the message
Message is sent by a WsProxy with SimpMessagingTemplate:
@Component public class WsProxy { private SimpMessagingTemplate messagingTemplate; @Autowired public WsProxy(SimpMessagingTemplate messagingTemplate) { this.messagingTemplate = messagingTemplate; } public void sendMessage(String clientId, String payload){ messagingTemplate.convertAndSend("/queue/" + clientId, payload); } }
On some machines it’s also good to wait until the connection is fully established so don’t hesitate to add good old:
Thread.currentThread().sleep(1000);
Testing the result asynchronously
The code in test is async so I pass the Future and wait until it completes with the expected result, or to fail test after timeout on waiting for the response, verifying its body:
assertThat(resultKeeper.get(2, SECONDS)).isEqualTo("test-payload");
That’s it
Now you can run the test, it will start your app, send a message, receive it and verify the contents. Which is everything you need to implement the WebSockets.
Source Code
I’m sure that seeing the source code will make you understand the article better. Grab it from my GitHub: https://github.com/yacekmm/looksok/tree/WebSocketDemo/Spring/WebSocket