TestingAPIs is still being built. If something breaks, email support@testingapis.com.

Testing streams the server pushes

Sequence numbers, timing, the last message, and data that repeats.

Plenty of sockets are mostly one-way: the client connects, maybe says what it wants, and the server talks from then on. Price feeds, live scores, progress updates and notification streams all work like this. This lesson uses /ws/tickersign in to see, which pushes one stock price a second, to look at how you test a stream you can't control.

What the ticker sends

Connect with ?count=3 and the ticker sends three prices, a completion message, and closes. Here is a real run, with the time of each event since the connection opened:

Events
+0.00s  open
+0.00s  {"type":"tick","data":{"tick":1,"symbol":"AAPL","name":"Apple Inc.","price":230.19,"change":2.67,"changePercent":1.17,"volume":10137,"timestamp":"2026-10-11T07:43:49.669Z"}}
+0.99s  {"type":"tick","data":{"tick":2,"symbol":"MSFT","name":"Microsoft Corp.","price":444.51,"change":6.40,"changePercent":1.46,"volume":10274,"timestamp":"2026-10-11T07:43:50.672Z"}}
+1.99s  {"type":"tick","data":{"tick":3,"symbol":"GOOGL","name":"Alphabet Inc.","price":177.47,"change":1.13,"changePercent":0.64,"volume":10411,"timestamp":"2026-10-11T07:43:51.674Z"}}
+1.99s  {"type":"complete","ticks":3}
+3.02s  close 1000 "Ticker finished"

Try it. The panel asks for five ticks; edit count in the URL for more, up to 300, or press Disconnect to stop early.

Messages appear here.

This socket is for signed-in members.Sign inCreate a free account

Formatted, one tick looks like this:

Message
{
  "type": "tick",
  "data": {
    "tick": 2,
    "symbol": "MSFT",
    "name": "Microsoft Corp.",
    "price": 444.51,
    "change": 6.40,
    "changePercent": 1.46,
    "volume": 10274,
    "timestamp": "2026-10-11T07:43:50.672Z"
  }
}

Every message has a type. Ticks carry their data in a data object with a sequence number, tick, that counts up from 1. The last message is complete and says how many ticks were sent. Then the server closes with 1000 and the reason Ticker finished. That's a well-designed stream: each message says what it is, the sequence number lets a client spot gaps, and the stream announces its own end instead of just stopping.

What to check

You can't test a stream message by message the way you test request and response. You collect what arrives over a period, then check the collection. These are the things to assert, roughly in order of how often they catch bugs:

CheckOn this ticker
Sequence: no gaps, no repeats, starts where it shouldtick goes 1, 2, 3 … up to count
The end: a final message, then the right close codecomplete with the right count, then 1000 Ticker finished
Count: as many items as were asked forExactly count ticks
Shape: every message parses and has its fieldsEvery tick has all eight data fields, with the right types
Timing: roughly the promised rhythmAbout one second between ticks
Values: each one makes sense on its own and with the otherschangePercent agrees with change and price

The last row is easy to skip and often productive. On tick 1, change is 2.67 and price is 230.19, so the previous price was 227.52 and the change was 1.17%, which is what changePercentsays. Fields that are calculated from each other should agree, and when a feed is assembled from several sources they sometimes don't.

A test that waits properly

The simplest reliable approach for a stream that ends by itself is to collect everything until the close, with an overall timeout so a stream that never ends fails the test instead of hanging it:

JavaScript
function collect(url, { timeoutMs = 10000 } = {}) {
  return new Promise((resolve, reject) => {
    const ws = new WebSocket(url);
    const messages = [];
    const timer = setTimeout(() => {
      ws.close();
      reject(new Error(`no close after ${timeoutMs} ms; got ${messages.length} messages`));
    }, timeoutMs);

    ws.addEventListener("message", (e) => {
      messages.push({ at: Date.now(), msg: JSON.parse(e.data) });
    });
    ws.addEventListener("close", (e) => {
      clearTimeout(timer);
      resolve({ messages, code: e.code, reason: e.reason });
    });
  });
}
JavaScript
const { messages, code, reason } = await collect("wss://api.testingapis.com/ws/ticker?count=5");

const ticks = messages.filter((m) => m.msg.type === "tick");
const last = messages.at(-1).msg;

// The sequence: 1, 2, 3, 4, 5 with nothing missing or repeated
assert.deepEqual(ticks.map((t) => t.msg.data.tick), [1, 2, 3, 4, 5]);

// The end: a completion message that agrees with what arrived, then a normal close
assert.deepEqual(last, { type: "complete", ticks: 5 });
assert.equal(code, 1000);
assert.equal(reason, "Ticker finished");

// The rhythm: about a second apart, with room for a slow network
for (let i = 1; i < ticks.length; i++) {
  const gap = ticks[i].at - ticks[i - 1].at;
  assert.ok(gap > 700 && gap < 1500, `gap ${i} was ${gap} ms`);
}
This example uses endpoints for signed-in members.Sign inCreate a free account

The timing check allows 700 to 1,500 milliseconds for a one-second interval. In the runs measured for this lesson the real gaps were between 991 and 1,002 milliseconds, but a test run on a busy CI machine over a slow network will see more spread. Tight timing assertions are the second most common cause of flaky stream tests, after fixed sleeps. Decide what the product actually promises ("about once a second", "at least every five seconds") and test that.

Look closely at the run at the top and you'll see the first tick arrives immediately, not after a second, and the completion message arrives in the same instant as the last tick. A test that expects count seconds before completion will be off by one interval. Those are the kinds of details to write down once and check every time.

Parameters and limits

The ticker takes one parameter, count, documented as 1 to 300. Testing the edges:

  • count=3, 45, 55, 70: exactly that many ticks, then a normal close.
  • count=0: one tick and a completion saying "ticks": 1. Out-of-range values are raised to the minimum instead of rejected.
  • count=999: 300 ticks over five minutes, then a normal completion. Values above the maximum are lowered to it.
  • count=abc: the handshake fails with a 400, so a browser sees only an error and close code 1006, as the handshake lesson showed.
  • Messages sent to the ticker are ignored. It keeps ticking.
  • Closing from the client stops it: the server answers with 1000 and the reason Closed by client, and sends no more ticks.

Clamping quietly, rather than rejecting, is a reasonable design for a feed. But it means a client that asks for 0 gets something it didn't ask for and no warning. Whichever way an API goes, check that the docs match.

Data that repeats

Connect twice and compare. Every connection starts at tick 1 with Apple at 230.19, then Microsoft at 444.51, then Alphabet at 177.47. The prices are the same on every connection; only the timestamps differ. The ticker is deterministic, which is ideal for tests: you can assert exact values.

It also means the stream has no memory. If a client drops after tick 20 and reconnects, it gets tick 1 again, not tick 21. A real price feed would send the current price instead, but the problem is the same: after a reconnect, the client has to decide what it already has, what it missed, and what to ignore. That's the subject of the reconnecting lesson.

Exercise

Connect to the ticker with count=10 and save every message. Check, without looking at the values first: are the sequence numbers 1 to 10? Does every changePercent match its change and price to two decimal places? Is every timestamplater than the one before? Report anything that doesn't hold, with the message that broke it.

Practise and go further