WebSocket and Webhook Testing: Real-Time APIs Explained

WebSocket and Webhook Testing: Real-Time APIs Explained

Not every API waits for you to ask. Live cricket scores, stock tickers, ride tracking and chat are pushed to you over WebSockets; payment confirmations, shipping updates and GitHub events are pushed to your server as webhooks. Both break in ways request-response testing never sees: dropped connections, out-of-order messages, duplicate events, forged signatures. This lesson shows you how to test both, with live echo servers and runnable Playwright code.

Quick answerWebSocket testing checks the connection handshake (HTTP 101), authentication on connect, message format and order, server pushes, heartbeats, reconnection after drops, and behaviour under many connections. Webhook testing checks that the provider sends the right event payload to your URL, that your endpoint verifies the signature, responds quickly with 2xx, handles retries and duplicate deliveries idempotently, tolerates out-of-order events and rejects replayed or tampered requests.
Advertisement

Polling vs WebSocket vs Webhook

Real-life example: waiting for a parcel Polling is calling the courier every 5 minutes: “Is it here yet?” WebSocket is staying on an open phone line with the courier, who tells you every update the moment it happens. Webhook is giving the courier your doorbell: they ring it once when the parcel arrives, and you don’t have to wait on the line.
Polling (REST)WebSocketWebhook
DirectionClient asks repeatedlyTwo-way, persistent connectionServer calls your URL on events
LatencyUp to the polling intervalMillisecondsSeconds
Typical useSimple status checksChat, live scores, trading, ride tracking, multiplayerPayments, CI events, CRM updates, shipping status
Who connectsClient → serverClient → server, then both sendProvider → your server

WebSocket Basics for Testers

  1. The client sends an HTTP request with Upgrade: websocket to a wss:// URL.
  2. The server answers 101 Switching Protocols. The connection stays open.
  3. Both sides exchange messages (text, usually JSON, or binary) in any order.
  4. Either side closes with a close code (1000 normal, 1001 going away, 1006 abnormal, 1008 policy violation, 1011 server error).

Many apps add a protocol on top: subscribe/unsubscribe messages, heartbeats (ping/pong), message ids and acknowledgements. Socket.IO, STOMP and GraphQL subscriptions are common higher-level protocols.

Advertisement

Try It: Public Echo Servers

Two free echo servers send back whatever you send, which is perfect for learning:

  • wss://echo.websocket.org: first sends a greeting like Request served by 4d896d95b55478, then echoes each message.
  • wss://ws.postman-echo.com/raw: echoes immediately.

In Postman

  1. New → WebSocket. URL: wss://ws.postman-echo.com/raw. Click Connect.
  2. Type {"event":"subscribe","match":"IND-vs-AUS"} and Send.
  3. The Messages pane shows sent (up arrow) and received (down arrow) messages with timestamps.
  4. Click Disconnect and check the close event.

In the browser console

const ws = new WebSocket('wss://ws.postman-echo.com/raw');
ws.onopen = () => ws.send(JSON.stringify({ ping: 1 }));
ws.onmessage = (e) => console.log('received', e.data);   // received {"ping":1}
ws.onclose = (e) => console.log('closed', e.code);

WebSocket Test Cases

AreaTestExpected
HandshakeConnect to the URL101; protocol/subprotocol agreed
AuthConnect without token / expired tokenRejected (401 on handshake or close 1008)
Message formatSubscribe to a topicAck message in the documented schema
Server pushTrigger an event (e.g. place an order)Update arrives within the SLA (e.g. 2 s)
OrderingSeveral rapid eventsReceived in order, or sequence numbers allow reordering
Invalid inputMalformed JSON, unknown actionError message, connection stays healthy (or documented close)
HeartbeatIdle for 60 sPing/pong keeps it alive; dead connections detected
ReconnectDrop the networkClient reconnects with back-off and resubscribes; no lost or duplicated updates
AuthorizationSubscribe to another user’s channelRefused
LoadThousands of concurrent connectionsStable latency and memory (k6, Artillery)
Advertisement

Automating WebSocket Tests in Playwright

For a pure API-level test, use the WebSocket class built into Node.js 22+ inside a Playwright test. A small helper turns events into promises:

import { test, expect } from '@playwright/test';

function openSocket(url: string): Promise<WebSocket> {
  return new Promise((resolve, reject) => {
    const ws = new WebSocket(url);
    ws.onopen = () => resolve(ws);
    ws.onerror = () => reject(new Error(`could not connect to ${url}`));
  });
}

function nextMessage(ws: WebSocket, timeoutMs = 5_000): Promise<string> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error('no message received')), timeoutMs);
    ws.addEventListener('message', (e) => { clearTimeout(timer); resolve(String(e.data)); }, { once: true });
  });
}

test('echo server returns the exact JSON we send', async () => {
  const ws = await openSocket('wss://ws.postman-echo.com/raw');
  const msg = { event: 'subscribe', match: 'IND-vs-AUS', id: Date.now() };

  const reply = nextMessage(ws);
  ws.send(JSON.stringify(msg));
  expect(JSON.parse(await reply)).toEqual(msg);

  ws.close(1000);
});

test('messages come back in order', async () => {
  const ws = await openSocket('wss://ws.postman-echo.com/raw');
  const received: string[] = [];
  const done = new Promise<void>((resolve) => {
    ws.onmessage = (e) => { received.push(String(e.data)); if (received.length === 5) resolve(); };
  });
  for (let i = 1; i <= 5; i++) ws.send(`ball-${i}`);
  await done;
  expect(received).toEqual(['ball-1', 'ball-2', 'ball-3', 'ball-4', 'ball-5']);
  ws.close();
});

For web apps, Playwright can also watch the browser’s own sockets with page.on('websocket', ws => ws.on('framereceived', ...)) and mock a socket server with page.routeWebSocket(), which is useful when testing how the UI reacts to pushed messages.

Webhook Basics for Testers

Real-life example: online payment A customer pays on a payment gateway’s page. The gateway POSTs an event like payment.captured to the store’s webhook URL with a JSON body and a signature header. The store verifies the signature, marks the order paid and replies 200. If the store’s server is down, the gateway retries later, so the same event can arrive twice and the store must not ship two parcels.

A typical webhook request your server receives:

POST /webhooks/payments HTTP/1.1
Content-Type: application/json
X-Signature: 5d7c3e...a91f            (HMAC-SHA256 of the raw body with a shared secret)
X-Event-Id: evt_01JD8K2Q
X-Timestamp: 1794482821

{"event":"payment.captured","payment_id":"pay_123","order_id":"ord_987","amount":229746,"currency":"INR"}
Advertisement

Webhook Test Cases

AreaTestExpected
DeliveryTrigger the event in the provider’s test modeYour URL receives the correct payload and headers
Signature validCorrect HMAC2xx; order updated
Signature invalid / missingChange one byte of the body401/400; nothing processed
ReplayResend an old request with an old timestampRejected (timestamp tolerance, e.g. 5 minutes)
DuplicatesSame event id twiceProcessed once (idempotent); second returns 2xx
Orderingpayment.refunded before payment.capturedHandled correctly (state checks or re-fetch)
RetriesYour endpoint returns 500 / times outProvider retries with back-off; eventual success processes once
SpeedMeasure response timeFast 2xx (heavy work queued), within the provider’s timeout
Unknown eventsEvent type you don’t handle2xx and ignored, not 500
SecurityHTTPS only; secret not loggedAs configured

Tools for capturing webhooks

  • webhook.site / RequestBin: get a public URL and see every request sent to it. Don’t send real customer data there.
  • ngrok / Cloudflare Tunnel: expose your local server to a provider during development.
  • Provider CLIs and dashboards: many payment providers can resend events and show delivery logs.
Advertisement

Testing Webhook Signatures in Code

Most providers sign webhooks with HMAC-SHA256 over the raw body (sometimes with the timestamp). Your receiver recomputes the signature and compares it in constant time. Here’s a self-contained test of that logic:

import { test, expect } from '@playwright/test';
import crypto from 'node:crypto';

const SECRET = 'whsec_test_123';   // test secret; real secrets come from environment variables

const sign = (rawBody: string, timestamp: number) =>
  crypto.createHmac('sha256', SECRET).update(`${timestamp}.${rawBody}`).digest('hex');

function verify(rawBody: string, timestamp: number, signature: string, now = Date.now() / 1000): boolean {
  if (Math.abs(now - timestamp) > 300) return false;                       // replay window: 5 minutes
  const expected = Buffer.from(sign(rawBody, timestamp), 'hex');
  const given = Buffer.from(signature, 'hex');
  return given.length === expected.length && crypto.timingSafeEqual(given, expected);
}

const body = JSON.stringify({ event: 'payment.captured', order_id: 'ord_987', amount: 229746 });
const ts = Math.floor(Date.now() / 1000);

test('valid signature is accepted', () => {
  expect(verify(body, ts, sign(body, ts))).toBe(true);
});

test('tampered body is rejected', () => {
  const tampered = body.replace('229746', '100');
  expect(verify(tampered, ts, sign(body, ts))).toBe(false);
});

test('old (replayed) request is rejected', () => {
  const old = ts - 3600;
  expect(verify(body, old, sign(body, old))).toBe(false);
});

test('duplicate delivery is processed once', () => {
  const processed = new Set<string>();
  const handle = (eventId: string) => { if (processed.has(eventId)) return 'duplicate'; processed.add(eventId); return 'processed'; };
  expect(handle('evt_01JD8K2Q')).toBe('processed');
  expect(handle('evt_01JD8K2Q')).toBe('duplicate');
});

In an integration test, the same sign() helper lets you POST signed fake events straight to your own webhook endpoint with request.post(), so you don’t need the real provider for every run.

Advertisement

Common Bugs in Real-Time APIs

  • WebSocket server accepts connections without authentication.
  • Client never reconnects after the phone switches from Wi-Fi to mobile data.
  • Messages arrive out of order during bursts; UI shows an old score.
  • Memory leak: server keeps dead connections forever.
  • Webhook signature not checked, or checked on a re-serialised body instead of the raw body.
  • Duplicate webhook ships an order twice or credits a wallet twice.
  • Webhook handler does slow work synchronously and times out, triggering endless retries.
Advertisement

Interview Questions: WebSocket and Webhook Testing

What is the difference between a WebSocket and a webhook?

A WebSocket is a persistent two-way connection opened by the client, used for real-time messaging. A webhook is a one-way HTTP POST sent by a provider to a URL you registered when an event occurs.

How do you test a WebSocket API?

Verify the handshake (101), authentication on connect, subscribe and message schemas, server pushes within SLA, message ordering, invalid message handling, heartbeats, reconnection after network loss, channel authorization and behaviour under many concurrent connections.

What status code indicates a successful WebSocket handshake?

101 Switching Protocols.

How do you test webhooks?

Trigger events in the provider’s test mode or send signed fake events, verify payloads and headers, signature validation, quick 2xx responses, idempotent handling of duplicates, out-of-order events, retries after failures and replay protection.

Why must webhook handlers be idempotent?

Providers retry when they don’t get a timely 2xx, and networks can duplicate deliveries, so the same event may arrive more than once. Storing processed event ids prevents double processing.

How is a webhook signature verified?

The receiver recomputes an HMAC (usually SHA-256) of the raw request body, often with a timestamp, using the shared secret, and compares it with the signature header in constant time. It also rejects old timestamps to prevent replays.

Which tools do you use to test WebSockets?

Postman’s WebSocket requests, browser DevTools (Network > WS), Playwright (Node WebSocket client, page.on(‘websocket’), routeWebSocket), and k6 or Artillery for load.

How do you test webhooks locally?

Use tunnels such as ngrok to expose a local server, inspection services like webhook.site, the provider’s event replay tools, or send signed test payloads directly to your endpoint from automated tests.

What are WebSocket close codes?

Numbers sent when closing a connection, such as 1000 normal closure, 1001 going away, 1006 abnormal closure without a close frame, 1008 policy violation and 1011 internal error. Tests check the right code is used.

What would you check if live updates sometimes show stale data?

Message ordering and sequence numbers, reconnection and resubscription logic, missed messages during disconnects, caching on the client, and whether the client re-fetches state after reconnecting.

Advertisement
View the full API testing syllabus