WebSocket Testing with Java: Complete Guide

Introduction

WebSocket testing in Java involves verifying both WebSocket client and server implementations. This guide covers comprehensive testing strategies using popular Java testing frameworks and tools.

Dependencies and Setup

1. Maven Dependencies

<properties>
<spring-boot.version>3.2.0</spring-boot.version>
<jetty.version>11.0.18</jetty.version>
<junit.version>5.10.1</junit.version>
<testcontainers.version>1.19.3</testcontainers.version>
</properties>
<dependencies>
<!-- Spring Boot WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring-boot.version}</version>
<scope>test</scope>
</dependency>
<!-- Jetty WebSocket Client -->
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-jetty-client</artifactId>
<version>${jetty.version}</version>
<scope>test</scope>
</dependency>
<!-- Java WebSocket API -->
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope>
</dependency>
<!-- Tyrus WebSocket Reference Implementation -->
<dependency>
<groupId>org.glassfish.tyrus</groupId>
<artifactId>tyrus-container-grizzly-client</artifactId>
<version>2.1.3</version>
<scope>test</scope>
</dependency>
<!-- TestContainers for Integration Testing -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<!-- Awaitility for Async Testing -->
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.3</version>
</dependency>
</dependencies>

2. Spring Boot WebSocket Configuration

package com.example.websocket.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic", "/queue");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*");
}
}

WebSocket Server Implementation

3. STOMP Message Controller

package com.example.websocket.controller;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SendToUser;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;
import java.security.Principal;
import java.time.LocalDateTime;
@Controller
public class WebSocketController {
private final SimpMessagingTemplate messagingTemplate;
public WebSocketController(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
@MessageMapping("/chat")
@SendTo("/topic/messages")
public ChatMessage broadcastMessage(ChatMessage message, Principal principal) {
String username = principal != null ? principal.getName() : "anonymous";
return new ChatMessage(
username,
HtmlUtils.htmlEscape(message.getContent()),
LocalDateTime.now()
);
}
@MessageMapping("/private-chat")
@SendToUser("/queue/private")
public ChatMessage privateMessage(ChatMessage message, Principal principal) {
return new ChatMessage(
principal.getName(),
"Private: " + HtmlUtils.htmlEscape(message.getContent()),
LocalDateTime.now()
);
}
@MessageMapping("/notification")
public void sendNotification(Notification notification) {
messagingTemplate.convertAndSend("/topic/notifications", 
new Notification(notification.getType(), notification.getMessage(), LocalDateTime.now()));
}
public static class ChatMessage {
private String from;
private String content;
private LocalDateTime timestamp;
// Constructors, getters, and setters
public ChatMessage() {}
public ChatMessage(String from, String content, LocalDateTime timestamp) {
this.from = from;
this.content = content;
this.timestamp = timestamp;
}
// Getters and setters
public String getFrom() { return from; }
public void setFrom(String from) { this.from = from; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}
public static class Notification {
private String type;
private String message;
private LocalDateTime timestamp;
// Constructors, getters, and setters
public Notification() {}
public Notification(String type, String message, LocalDateTime timestamp) {
this.type = type;
this.message = message;
this.timestamp = timestamp;
}
// Getters and setters
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}
}

WebSocket Testing Framework

4. Base Test Configuration

package com.example.websocket.test.config;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandler;
import org.springframework.web.socket.client.standard.StandardWebSocketClient;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import org.springframework.web.socket.sockjs.client.SockJsClient;
import org.springframework.web.socket.sockjs.client.WebSocketTransport;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BaseWebSocketTest {
@LocalServerPort
protected int port;
protected String getWebSocketUrl() {
return "ws://localhost:" + port + "/ws";
}
protected String getSockJsUrl() {
return "http://localhost:" + port + "/ws";
}
protected WebSocketStompClient createWebSocketStompClient() {
WebSocketStompClient stompClient = new WebSocketStompClient(
new SockJsClient(List.of(new WebSocketTransport(new StandardWebSocketClient())))
);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
return stompClient;
}
protected StompSession createStompSession(StompSessionHandler sessionHandler) 
throws ExecutionException, InterruptedException, TimeoutException {
WebSocketStompClient stompClient = createWebSocketStompClient();
return stompClient.connectAsync(getWebSocketUrl(), sessionHandler)
.get(5, TimeUnit.SECONDS);
}
protected <T> T awaitCompletableFuture(CompletableFuture<T> future) {
try {
return future.get(10, TimeUnit.SECONDS);
} catch (Exception e) {
throw new RuntimeException("Timeout waiting for WebSocket operation", e);
}
}
}

5. STOMP Client Test Framework

package com.example.websocket.test.client;
import org.springframework.messaging.simp.stomp.StompFrameHandler;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
import java.lang.reflect.Type;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
public class TestStompSessionHandler extends StompSessionHandlerAdapter {
private final CompletableFuture<StompSession> connectFuture;
private final BlockingQueue<Object> receivedMessages;
private final String subscriptionDestination;
public TestStompSessionHandler(String subscriptionDestination) {
this.connectFuture = new CompletableFuture<>();
this.receivedMessages = new LinkedBlockingQueue<>();
this.subscriptionDestination = subscriptionDestination;
}
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
connectFuture.complete(session);
// Subscribe to destination if specified
if (subscriptionDestination != null) {
session.subscribe(subscriptionDestination, new StompFrameHandler() {
@Override
public Type getPayloadType(StompHeaders headers) {
return Object.class;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
receivedMessages.offer(payload);
}
});
}
}
public StompSession awaitConnection() throws InterruptedException, java.util.concurrent.TimeoutException {
return connectFuture.get(10, TimeUnit.SECONDS);
}
public <T> T awaitMessage(Class<T> messageType) throws InterruptedException {
try {
Object message = receivedMessages.poll(10, TimeUnit.SECONDS);
if (message != null && messageType.isInstance(message)) {
return messageType.cast(message);
}
throw new RuntimeException("Expected message of type " + messageType + " but got: " + message);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for message", e);
}
}
public void sendMessage(StompSession session, String destination, Object payload) {
session.send(destination, payload);
}
}

Unit and Integration Tests

6. WebSocket Controller Unit Tests

package com.example.websocket.test.unit;
import com.example.websocket.controller.WebSocketController;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import java.security.Principal;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class WebSocketControllerUnitTest {
@Mock
private SimpMessagingTemplate messagingTemplate;
@Mock
private Principal principal;
private WebSocketController controller;
@BeforeEach
void setUp() {
controller = new WebSocketController(messagingTemplate);
}
@Test
void testBroadcastMessage() {
// Given
WebSocketController.ChatMessage inputMessage = 
new WebSocketController.ChatMessage("user", "Hello World!", null);
when(principal.getName()).thenReturn("testuser");
// When
WebSocketController.ChatMessage result = 
controller.broadcastMessage(inputMessage, principal);
// Then
assertThat(result.getFrom()).isEqualTo("testuser");
assertThat(result.getContent()).isEqualTo("Hello World!");
assertThat(result.getTimestamp()).isNotNull();
}
@Test
void testPrivateMessage() {
// Given
WebSocketController.ChatMessage inputMessage = 
new WebSocketController.ChatMessage("user", "Private message", null);
when(principal.getName()).thenReturn("testuser");
// When
WebSocketController.ChatMessage result = 
controller.privateMessage(inputMessage, principal);
// Then
assertThat(result.getFrom()).isEqualTo("testuser");
assertThat(result.getContent()).startsWith("Private:");
assertThat(result.getTimestamp()).isNotNull();
}
@Test
void testSendNotification() {
// Given
WebSocketController.Notification notification = 
new WebSocketController.Notification("INFO", "Test notification", null);
// When
controller.sendNotification(notification);
// Then
verify(messagingTemplate).convertAndSend(
eq("/topic/notifications"), 
any(WebSocketController.Notification.class)
);
}
}

7. STOMP Integration Tests

package com.example.websocket.test.integration;
import com.example.websocket.controller.WebSocketController;
import com.example.websocket.test.client.TestStompSessionHandler;
import com.example.websocket.test.config.BaseWebSocketTest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
@SpringBootTest
class StompIntegrationTest extends BaseWebSocketTest {
@Test
void testWebSocketConnection() throws Exception {
// Given
TestStompSessionHandler sessionHandler = new TestStompSessionHandler(null);
// When
StompSession session = createStompSession(sessionHandler);
// Then
assertThat(session).isNotNull();
assertThat(session.isConnected()).isTrue();
}
@Test
void testBroadcastMessage() throws Exception {
// Given
TestStompSessionHandler sessionHandler = 
new TestStompSessionHandler("/topic/messages");
StompSession session = createStompSession(sessionHandler);
WebSocketController.ChatMessage testMessage = 
new WebSocketController.ChatMessage("testuser", "Hello everyone!", null);
// When
session.send("/app/chat", testMessage);
// Then
WebSocketController.ChatMessage receivedMessage = 
sessionHandler.awaitMessage(WebSocketController.ChatMessage.class);
assertThat(receivedMessage.getFrom()).isEqualTo("testuser");
assertThat(receivedMessage.getContent()).isEqualTo("Hello everyone!");
}
@Test
void testMultipleClients() throws Exception {
// Given
TestStompSessionHandler client1Handler = 
new TestStompSessionHandler("/topic/messages");
TestStompSessionHandler client2Handler = 
new TestStompSessionHandler("/topic/messages");
StompSession client1 = createStompSession(client1Handler);
StompSession client2 = createStompSession(client2Handler);
WebSocketController.ChatMessage testMessage = 
new WebSocketController.ChatMessage("user1", "Broadcast message", null);
// When
client1.send("/app/chat", testMessage);
// Then - Both clients should receive the message
WebSocketController.ChatMessage client1Message = 
client1Handler.awaitMessage(WebSocketController.ChatMessage.class);
WebSocketController.ChatMessage client2Message = 
client2Handler.awaitMessage(WebSocketController.ChatMessage.class);
assertThat(client1Message.getContent()).isEqualTo("Broadcast message");
assertThat(client2Message.getContent()).isEqualTo("Broadcast message");
}
}

Advanced Testing Scenarios

8. Error Handling and Reconnection Tests

package com.example.websocket.test.advanced;
import com.example.websocket.test.config.BaseWebSocketTest;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
class ErrorHandlingTest extends BaseWebSocketTest {
@Test
void testConnectionFailure() {
// Given
WebSocketStompClient stompClient = createWebSocketStompClient();
TestStompSessionHandler sessionHandler = new TestStompSessionHandler(null);
// When & Then - Try to connect to invalid URL
assertThatThrownBy(() -> 
stompClient.connectAsync("ws://invalid-host:9999/ws", sessionHandler)
.get(5, TimeUnit.SECONDS)
).isInstanceOf(Exception.class);
}
@Test
void testMessageSerializationError() throws Exception {
// Given
TestStompSessionHandler sessionHandler = new TestStompSessionHandler(null);
StompSession session = createStompSession(sessionHandler);
// When - Send invalid message (object that can't be serialized)
Object invalidMessage = new Object() {
@SuppressWarnings("unused")
public String getData() {
throw new RuntimeException("Serialization error");
}
};
// Then - Message should be sent without crashing the server
session.send("/app/chat", invalidMessage);
// Verify connection is still alive
assertThat(session.isConnected()).isTrue();
}
@Test
void testAuthentication() throws Exception {
// Given
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
headers.add("Authorization", "Bearer test-token");
WebSocketStompClient stompClient = createWebSocketStompClient();
TestStompSessionHandler sessionHandler = new TestStompSessionHandler(null);
// When
StompSession session = stompClient.connectAsync(getWebSocketUrl(), headers, sessionHandler)
.get(5, TimeUnit.SECONDS);
// Then
assertThat(session).isNotNull();
assertThat(session.isConnected()).isTrue();
}
}

9. Performance and Load Testing

package com.example.websocket.test.performance;
import com.example.websocket.controller.WebSocketController;
import com.example.websocket.test.client.TestStompSessionHandler;
import com.example.websocket.test.config.BaseWebSocketTest;
import org.junit.jupiter.api.Test;
import org.springframework.messaging.simp.stomp.StompSession;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.assertj.core.api.Assertions.assertThat;
class PerformanceTest extends BaseWebSocketTest {
@Test
void testConcurrentConnections() throws Exception {
// Given
int connectionCount = 10;
ExecutorService executor = Executors.newFixedThreadPool(connectionCount);
List<CompletableFuture<StompSession>> futures = new ArrayList<>();
// When
for (int i = 0; i < connectionCount; i++) {
CompletableFuture<StompSession> future = CompletableFuture.supplyAsync(() -> {
try {
TestStompSessionHandler handler = new TestStompSessionHandler(null);
return createStompSession(handler);
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executor);
futures.add(future);
}
// Then
CompletableFuture<Void> allFutures = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
List<StompSession> sessions = allFutures.thenApply(v -> 
futures.stream()
.map(CompletableFuture::join)
.toList()
).get(30, TimeUnit.SECONDS);
assertThat(sessions).hasSize(connectionCount);
sessions.forEach(session -> 
assertThat(session.isConnected()).isTrue()
);
executor.shutdown();
}
@Test
void testHighFrequencyMessages() throws Exception {
// Given
TestStompSessionHandler sessionHandler = 
new TestStompSessionHandler("/topic/messages");
StompSession session = createStompSession(sessionHandler);
int messageCount = 100;
AtomicInteger receivedCount = new AtomicInteger(0);
// Start counting received messages in background
CompletableFuture<Void> receptionFuture = CompletableFuture.runAsync(() -> {
for (int i = 0; i < messageCount; i++) {
try {
sessionHandler.awaitMessage(WebSocketController.ChatMessage.class);
receivedCount.incrementAndGet();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
// When - Send messages rapidly
for (int i = 0; i < messageCount; i++) {
WebSocketController.ChatMessage message = 
new WebSocketController.ChatMessage("user", "Message " + i, null);
session.send("/app/chat", message);
}
// Then
receptionFuture.get(10, TimeUnit.SECONDS);
assertThat(receivedCount.get()).isEqualTo(messageCount);
}
}

Jetty WebSocket Client Testing

10. Raw WebSocket Client Tests

package com.example.websocket.test.raw;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.StatusCode;
import org.eclipse.jetty.websocket.api.annotations.*;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
class RawWebSocketTest {
@WebSocket
public static class TestWebSocketClientEndpoint {
private final CompletableFuture<String> messageFuture = new CompletableFuture<>();
private final CompletableFuture<Void> connectFuture = new CompletableFuture<>();
private Session session;
@OnWebSocketConnect
public void onConnect(Session session) {
this.session = session;
connectFuture.complete(null);
}
@OnWebSocketMessage
public void onMessage(String message) {
messageFuture.complete(message);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
System.out.println("Connection closed: " + statusCode + " - " + reason);
}
@OnWebSocketError
public void onError(Throwable cause) {
messageFuture.completeExceptionally(cause);
}
public void sendMessage(String message) throws Exception {
session.getRemote().sendString(message);
}
public CompletableFuture<String> getMessageFuture() {
return messageFuture;
}
public CompletableFuture<Void> getConnectFuture() {
return connectFuture;
}
}
@Test
void testRawWebSocketConnection() throws Exception {
// Given
WebSocketClient client = new WebSocketClient();
client.start();
TestWebSocketClientEndpoint clientEndpoint = new TestWebSocketClientEndpoint();
// When
URI serverUri = URI.create("ws://localhost:8080/ws");
CompletableFuture<Session> connectFuture = client.connect(clientEndpoint, serverUri);
// Wait for connection
Session session = connectFuture.get(5, TimeUnit.SECONDS);
clientEndpoint.getConnectFuture().get(5, TimeUnit.SECONDS);
// Then
assertThat(session).isNotNull();
assertThat(session.isOpen()).isTrue();
// Cleanup
session.close(StatusCode.NORMAL, "Test complete");
client.stop();
}
}

TestContainers Integration

11. Docker-based Integration Tests

package com.example.websocket.test.docker;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
class DockerWebSocketTest {
@Container
private static final GenericContainer<?> websocketServer = 
new GenericContainer<>("my-websocket-app:latest")
.withExposedPorts(8080)
.withEnv("SERVER_PORT", "8080");
@Test
void testWebSocketServerInContainer() throws Exception {
// Given
String host = websocketServer.getHost();
Integer port = websocketServer.getFirstMappedPort();
URI serverUri = URI.create("ws://" + host + ":" + port + "/ws");
CompletableFuture<String> messageFuture = new CompletableFuture<>();
// When - Create WebSocket client
HttpClient client = HttpClient.newHttpClient();
WebSocket webSocket = client.newWebSocketBuilder()
.buildAsync(serverUri, new WebSocket.Listener() {
private StringBuilder messageBuffer = new StringBuilder();
@Override
public CompletionStage<?> onText(WebSocket webSocket, 
CharSequence data, boolean last) {
messageBuffer.append(data);
if (last) {
messageFuture.complete(messageBuffer.toString());
messageBuffer = new StringBuilder();
}
return WebSocket.Listener.super.onText(webSocket, data, last);
}
@Override
public void onOpen(WebSocket webSocket) {
webSocket.sendText("Test message", true);
WebSocket.Listener.super.onOpen(webSocket);
}
})
.get(10, TimeUnit.SECONDS);
// Then
String receivedMessage = messageFuture.get(10, TimeUnit.SECONDS);
assertThat(receivedMessage).isNotNull();
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Test complete");
}
}

Custom Test Utilities

12. WebSocket Test Utilities

package com.example.websocket.test.util;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandler;
import java.lang.reflect.Type;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
public class WebSocketTestUtils {
public static class FilteringStompFrameHandler implements StompSessionHandler {
private final BlockingQueue<Object> messages = new LinkedBlockingQueue<>();
private final Predicate<StompHeaders> filter;
private final Type payloadType;
public FilteringStompFrameHandler(Type payloadType, Predicate<StompHeaders> filter) {
this.payloadType = payloadType;
this.filter = filter;
}
@Override
public Type getPayloadType(StompHeaders headers) {
return payloadType;
}
@Override
public void handleFrame(StompHeaders headers, Object payload) {
if (filter.test(headers)) {
messages.offer(payload);
}
}
public <T> T awaitMessage(Class<T> messageType, long timeout, TimeUnit unit) 
throws InterruptedException {
Object message = messages.poll(timeout, unit);
if (message != null && messageType.isInstance(message)) {
return messageType.cast(message);
}
return null;
}
}
public static void subscribeWithFilter(StompSession session, String destination, 
FilteringStompFrameHandler handler) {
session.subscribe(destination, handler);
}
public static StompHeaders createHeaders(String... keyValuePairs) {
if (keyValuePairs.length % 2 != 0) {
throw new IllegalArgumentException("Key-value pairs must be even");
}
StompHeaders headers = new StompHeaders();
for (int i = 0; i < keyValuePairs.length; i += 2) {
headers.add(keyValuePairs[i], keyValuePairs[i + 1]);
}
return headers;
}
public static void waitForCondition(Condition condition, long timeout, TimeUnit unit) {
long endTime = System.currentTimeMillis() + unit.toMillis(timeout);
while (System.currentTimeMillis() < endTime) {
if (condition.isMet()) {
return;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Interrupted while waiting for condition", e);
}
}
throw new RuntimeException("Condition not met within timeout");
}
@FunctionalInterface
public interface Condition {
boolean isMet();
}
}

Configuration for Testing

13. Test Configuration Files

# application-test.yml
spring:
main:
web-application-type: servlet
jackson:
default-property-inclusion: non_null
serialization:
write-dates-as-timestamps: false
server:
port: 0  # Use random port for tests
logging:
level:
org.springframework.web.socket: DEBUG
org.springframework.messaging: DEBUG
com.example.websocket: DEBUG
websocket:
client:
connect-timeout: 5000
read-timeout: 10000

Best Practices

  1. Use Random Ports: Always use random ports for tests to avoid conflicts
  2. Proper Cleanup: Always close WebSocket connections after tests
  3. Timeout Management: Set appropriate timeouts for async operations
  4. Error Handling: Test both success and failure scenarios
  5. Concurrent Testing: Test with multiple clients to simulate real-world usage
  6. Message Validation: Verify both message content and headers
  7. Resource Management: Use try-with-resources for client objects

Conclusion

WebSocket testing in Java requires a comprehensive approach covering:

  • Unit Tests: Test individual WebSocket handlers and controllers
  • Integration Tests: Test full WebSocket communication flow
  • Performance Tests: Verify behavior under load
  • Error Scenarios: Test connection failures and error handling
  • Multiple Clients: Test with concurrent WebSocket connections

The provided examples demonstrate how to effectively test WebSocket applications using Spring's STOMP support, Jetty WebSocket client, and TestContainers for Docker-based testing. This approach ensures robust WebSocket functionality in production environments.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper