
Test a WebSocket API in Automated Tests and CI
Short answer: test a WebSocket API in CI by opening a connection, sending one message, waiting for the reply and asserting on it, then running the whole thing from the command line so a failure stops the build. The common answer is a client script or test runner started inside the pipeline, and that works with any tool.
This guide shows that procedure with DevTools flows for the connect, send and wait steps, and a plain client script for the assertion, because the docs do not show an assertion on received messages.
The procedure in four steps
A WebSocket test differs from a request and response test because the reply arrives later, on a connection that stays open. Four steps cover it:
- Connect to the
ws://orwss://endpoint, with any headers the handshake needs. - Send one message over that connection.
- Wait for the server to answer.
- Assert on the message that came back, and exit non-zero when it is wrong.
Steps 1 to 3 belong in a flow file that lives in version control, as in the guide to testing a GraphQL API with multi-step flows. Step 4 decides whether CI goes red. Running the file headless, covered further down, ties them together.
What DevTools ships for WebSockets
Flow nodes for WebSockets landed in March 2026, in the same change as the Wait node. The CLI changelog describes them as connection and send flow nodes with message capture, plus a Wait node that pauses a flow for a configurable duration.
The connection node takes a ws:// or wss:// URL and connection-time headers such as Authorization, and downstream send nodes write to it. Received messages are captured per connection and exposed with an index, a type and the payload.
One fix matters for CI. Before CLI 1.5.0 the connection stayed open only until its own step finished, so a later send could fail with "use of closed network connection". The connection now lasts for the whole flow, and its handshake has a 30 second limit. Use a current CLI.
Two things the sources do not show: how a WebSocket session recorded in a HAR file becomes a flow, and an example that asserts on a received message. The HAR stream detection that shipped in October covers text/event-stream entries, which are server-sent events, not WebSocket frames. This guide does not rely on either.
Steps 1 to 3: connect, send and wait in a flow file
The YAML below follows the WebSocket flow in the project's round-trip test corpus. It connects with an Authorization header, sends a ping and waits half a second.
workspace_name: WebSocket checks
flows:
- name: WebSocketFlow
steps:
- manual_start:
name: Start
- ws_connection:
name: Conn
depends_on: Start
url: wss://echo.example.com/socket
headers:
Authorization: Bearer token123
- ws_send:
name: SendPing
depends_on: Conn
ws_connection_node_name: Conn
message: '{"type":"ping"}'
- wait:
name: WaitForPong
depends_on: SendPing
duration_ms: '500'
Replace the URL with your own endpoint, and keep real tokens out of the file. The flows documentation covers variables, so the token can come from an environment value instead of a literal.
Each step does one job. ws_connection opens the socket. ws_send names the connection it writes to through ws_connection_node_name. depends_on fixes the order, and the wait step pauses for duration_ms, which here gives the server time to reply.
A fixed wait is the simplest choice and also the weakest one, because it guesses how long the server needs. The flakiness section below covers what to do about that.
Step 4: assert on the reply
The docs say received messages are captured per connection so you can assert on them. They do not show the expression that reads one. The If node branches on a boolean expression such as GetUser.response.status == 200, and the YAML form is an if step with a condition and an .else branch, but no example points that condition at a WebSocket message. Without a documented example, the dependable assertion is a small client script that fails the job itself.
This Node script uses the ws package. It sends the same ping, expects a pong within five seconds and sets the exit code:
import WebSocket from "ws";
const ws = new WebSocket(process.env.WS_URL);
const timer = setTimeout(() => {
console.error("no reply within 5s");
process.exit(1);
}, 5000);
ws.on("open", () => ws.send(JSON.stringify({ type: "ping" })));
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
if (msg.type !== "pong") {
console.error("unexpected reply", msg);
process.exit(1);
}
clearTimeout(timer);
ws.close();
process.exit(0);
});
ws.on("error", (err) => {
console.error(err.message);
process.exit(1);
});
Assert on the parts of the message that must not change, such as its type and required fields, and skip the full payload. The same advice applies to HTTP responses, and deterministic API assertions explains it in depth.
Run it in CI
The documented CI pattern installs the CLI with a script and runs the flow file, writing JUnit and JSON reports:
- name: Install DevTools CLI
run: curl -fsSL https://sh.dev.tools/install.sh | bash
- name: Run WebSocket flow
run: |
devtools flow run websocket.yaml \
--report junit:test-results.xml \
--report json:test-results.json
- name: Run reply assertion
run: node assert-pong.mjs
env:
WS_URL: wss://echo.example.com/socket
The --report option accepts console, json and junit, and the JUnit file is the one to hand to your CI system's test report collector, as the guide on JUnit reports in GitHub Actions shows. The CI integrations guide has the same pattern for other pipelines. The CLI is free, runs in your own CI and needs no account.
What makes WebSocket tests flaky in CI
Three causes account for most flaky WebSocket tests.
Connection lifetime. A connection that closes before the next step sends produces failures that look random. The CLI bug described above is one example. Check your CLI version before blaming the server.
Timeouts. A fixed pause either wastes time or runs out too early. In the client script, the five second timer fails fast with a clear message and does not hang the job. In a flow, the wait step is a fixed duration, so set it above the slowest reply you see on a loaded runner and keep the script's timer as the real limit.
Message order. A server that pushes a welcome frame, heartbeats or broadcasts can deliver something before your reply. Match on a message type or id, as the script does, instead of taking the first message.
Other approaches
Three other approaches are common:
- A client script in a test runner. This is the method used for step 4 above.
- A faked server in a unit test runner. A Stack Overflow thread asks how to test web sockets with Jasmine by faking the server and firing connect and disconnect events. This tests your client code, not the real API.
- JMeter. One walkthrough tests an AI chatbot over WebSockets using JMeter.
Pick the faked server when you are testing a client. Pick a real connection, as in the flow above, when the server contract is what you are protecting.
Common questions
Can DevTools import a WebSocket session from a HAR file?
The sources checked for this guide do not say so. HAR stream detection is described for text/event-stream entries, so build the flow by hand as above.
Is there a hosted option?
There is no hosted DevTools. The hosted option is Stresseur Cloud, a separate paid product from the same team, at stresseur.com.
Conclusion
Connect, send, wait and assert is the whole shape of a WebSocket test. Keep the first three steps in a flow file in your repository, use a short client script for the assertion until you have a documented one, and run both in the pipeline so a wrong reply fails the build.
