diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/TestWebSocketClient.java b/spring-graphql/src/test/java/org/springframework/graphql/client/TestWebSocketClient.java
new file mode 100644
index 00000000..93fc6ab2
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/client/TestWebSocketClient.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.graphql.client;
+
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.Assert;
+import org.springframework.web.reactive.socket.WebSocketHandler;
+import org.springframework.web.reactive.socket.client.WebSocketClient;
+
+/**
+ * {@link WebSocketClient} that uses {@link TestWebSocketConnection} to connect
+ * and test the interaction between a client and a server {@link WebSocketHandler}.
+ *
+ *
Call {@link #execute(URI, WebSocketHandler)}, subscribe, and then use
+ * getters to access established connections by index from based on order of
+ * execution.
+ *
+ * @author Rossen Stoyanchev
+ */
+public class TestWebSocketClient implements WebSocketClient {
+
+ private final WebSocketHandler serverHandler;
+
+ private final List connections = new CopyOnWriteArrayList<>();
+
+
+ public TestWebSocketClient(WebSocketHandler serverHandler) {
+ this.serverHandler = serverHandler;
+ }
+
+
+ /**
+ * Return the connection at the specified index from a list of connections
+ * based on order of execution.
+ */
+ public TestWebSocketConnection getConnection(int index) {
+ Assert.isTrue(index < this.connections.size(),
+ "No connection at index=" + index + ", total=" + this.connections.size());
+ return connections.get(index);
+ }
+
+ /**
+ * Return the number of connections which corresponds to the number of calls
+ * to one of the execute methods.
+ */
+ public int getConnectionCount() {
+ return this.connections.size();
+ }
+
+
+ @Override
+ public Mono execute(URI url, WebSocketHandler clientHandler) {
+ return execute(URI.create("/"), HttpHeaders.EMPTY, clientHandler);
+ }
+
+ @Override
+ public Mono execute(URI url, HttpHeaders headers, WebSocketHandler clientHandler) {
+ TestWebSocketConnection connection = new TestWebSocketConnection(url, headers);
+ this.connections.add(connection);
+ return connection.connect(clientHandler, this.serverHandler);
+ }
+
+}
diff --git a/spring-graphql/src/test/java/org/springframework/graphql/client/TestWebSocketConnection.java b/spring-graphql/src/test/java/org/springframework/graphql/client/TestWebSocketConnection.java
new file mode 100644
index 00000000..4cd31b8b
--- /dev/null
+++ b/spring-graphql/src/test/java/org/springframework/graphql/client/TestWebSocketConnection.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2002-2022 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.graphql.client;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.reactivestreams.Publisher;
+import reactor.core.Scannable;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.Sinks;
+
+import org.springframework.core.io.buffer.DataBuffer;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.web.reactive.socket.CloseStatus;
+import org.springframework.web.reactive.socket.HandshakeInfo;
+import org.springframework.web.reactive.socket.WebSocketHandler;
+import org.springframework.web.reactive.socket.WebSocketMessage;
+import org.springframework.web.reactive.socket.adapter.AbstractWebSocketSession;
+
+/**
+ * Emulates a WebSocket connection by connecting a client and a server handlers
+ * via two message {@link Sinks.Many sinks}, one for each end of the connection.
+ *
+ *
Use {@link TestWebSocketClient} to establish connections that can then be
+ * accessed through it.
+ *
+ * @author Rossen Stoyanchev
+ */
+public class TestWebSocketConnection {
+
+ private static final AtomicLong connectionIndex = new AtomicLong();
+
+
+ private final TestWebSocketSession clientSession;
+
+ private final TestWebSocketSession serverSession;
+
+
+ public TestWebSocketConnection(URI url, HttpHeaders headers) {
+
+ long id = connectionIndex.incrementAndGet();
+
+ Sinks.Many clientSink = Sinks.many().unicast().onBackpressureBuffer();
+ Sinks.Many serverSink = Sinks.many().unicast().onBackpressureBuffer();
+
+ Sinks.One clientStatusSink = Sinks.one();
+ Sinks.One serverStatusSink = Sinks.one();
+
+ this.clientSession = new TestWebSocketSession("client-session-" + id, url, headers,
+ clientSink, serverSink.asFlux(), clientStatusSink, serverStatusSink.asMono());
+
+ this.serverSession = new TestWebSocketSession("server-session-" + id, url, headers,
+ serverSink, clientSink.asFlux(), serverStatusSink, clientStatusSink.asMono());
+ }
+
+
+ /**
+ * Return {@code true} if both client and server sessions are open.
+ */
+ public boolean isOpen() {
+ return (this.clientSession.isOpen() && this.serverSession.isOpen());
+ }
+
+ /**
+ * Return messages sent from the client side.
+ */
+ public List getClientMessages() {
+ return this.clientSession.getSentMessages();
+ }
+
+ /**
+ * Return messages sent from the server side.
+ */
+ public List getServerMessages() {
+ return this.serverSession.getSentMessages();
+ }
+
+
+ /**
+ * Starts client and server session handling, and if either side errors or
+ * completes, close its session with either {@link CloseStatus#NORMAL} or
+ * {@link CloseStatus#PROTOCOL_ERROR} respectively.
+ * @param clientHandler the client session handler
+ * @param serverHandler the server session handler
+ * @return {@code Mono} that completes when either client or server
+ * session handling completes or when either is closed
+ */
+ Mono connect(WebSocketHandler clientHandler, WebSocketHandler serverHandler) {
+
+ Mono serverMono = invokeHandler(serverHandler, this.serverSession);
+ Mono clientMono = invokeHandler(clientHandler, this.clientSession);
+
+ Mono serverStatusMono = this.serverSession.closeStatus().then();
+ Mono clientStatusMono = this.clientSession.closeStatus().then();
+
+ return Mono.zip(serverMono, clientMono, serverStatusMono, clientStatusMono).then();
+ }
+
+ /**
+ * Handle the session and complete it when handling completes.
+ */
+ private Mono invokeHandler(WebSocketHandler serverHandler, TestWebSocketSession session) {
+ return serverHandler.handle(session)
+ .then(Mono.defer(() -> session.close(CloseStatus.NORMAL)))
+ .onErrorResume(ex -> session.close(CloseStatus.PROTOCOL_ERROR).then(Mono.error(ex)));
+ }
+
+
+ /**
+ * Close the connection from the client side.
+ */
+ public Mono closeClientSession(CloseStatus status) {
+ return this.clientSession.close(status);
+ }
+
+ /**
+ * Close the connection from the server side.
+ */
+ public Mono closeServerSession(CloseStatus status) {
+ return this.serverSession.close(status);
+ }
+
+ /**
+ * Return the {@code CloseStatus} that may have come from either side.
+ */
+ public Mono closeStatus() {
+ return this.clientSession.closeStatus().or(this.serverSession.closeStatus());
+ }
+
+
+ /**
+ * Test WebSocketSession that sends to a given {@link Sinks.Many sink} and
+ * receives from a given {@code Flux}.
+ */
+ private static class TestWebSocketSession extends AbstractWebSocketSession