Chaos testing
On your machine the network never fails. The server is a few milliseconds away, nothing drops, and every message arrives whole. So the code that handles a bad network, such as reconnecting, the send queue, heartbeat timeouts and invalid messages, rarely runs before your users hit it.
react-websocket/chaos makes the network bad on purpose. It's a WebSocket class that wraps the real one and adds latency, lost and corrupt messages, and refused, dropped and stalled connections. You pass it as the WebSocket option, so the client, your server and your app stay the same.
Set up
Create one chaos class and pass it to your clients in development:
import.meta.env.DEV is replaced with false in a production build, so the bundler removes the chaos class along with the condition. Use a condition the bundler can evaluate. A runtime flag works too, but it keeps the class (about 2 kB) in your bundle.
With Next.js and other server-rendering frameworks, also check for a browser, so the server never sees the option:
With hooks, pass it the same way: useWebSocket(url, { WebSocket: Chaos }). Hooks that share a URL share one client, and its options come from the first one to mount, so give every hook for that URL the same class.
Faults
Every fault is off until you set it.
A stall is what happens when a laptop sleeps or a NAT forgets the connection: the socket is dead but nothing says so. Without a heartbeat timeout, the client stays open forever, and send() keeps returning true while every message disappears. Try stall once on any app that stays open for long. To see a wake check, call Chaos.stall(), then switch tabs and back: the heartbeat pings at once and reconnects after timeout.
Trigger faults on demand
Random faults show you how the app copes over time. To look at one case, trigger it yourself. The class has static methods that act on every connection it made:
A small panel during development:
Or expose it on the console with globalThis.chaos = Chaos and type chaos.drop() while you watch the UI.
Some cases worth trying:
-
A server outage. Refuse every attempt, drop the connection, and see how the app looks after a few backoff rounds:
-
Rejected credentials.
Chaos.drop(4001, 'unauthorized')tests yourshouldReconnectand authentication handling. -
An overloaded server.
Chaos.drop(1013, '30')closes with Try Again Later and a 30-second delay, so you can check that your UI copes with a long wait. -
A deploy.
Chaos.inject('{"type":"reconnect","within":5000}')delivers your server's move notice, if that's its format, so you can watchmigrate()switch connections without the status leavingopen. -
A message your server doesn't send yet.
injecttakes any string or binary data. It isn't lost or corrupted, so you can try a new message type, or a valid message that fails yourschema.
configure merges into the current faults, so configure({ loss: 0.1 }) leaves latency alone. Each option is replaced whole: configure({ loss: { in: 0.1 } }) replaces an earlier { out }. Changing drop, stall or enabled restarts the timers of open connections. configure({ enabled: false }) turns off every random fault without forgetting the settings. The methods work whether it's enabled or not.
Reproducible runs
Pass a seed to get the same faults every time. Each connection has its own random sequence, so whether the third connection is refused, or when it drops, doesn't depend on how much traffic the first two carried. Faults on messages repeat when the same messages arrive in the same order.
A random seed per session, logged, lets you replay a run that went wrong:
The client's own reconnect jitter is random too. Set reconnect: { jitter: 0 } when you need exact timing.
In tests
The chaos class can wrap a mock WebSocket, so your tests can check how the app recovers. With fake timers:
Without faults, the chaos class passes everything through synchronously, so MockWebSocket.last.open() opens the client right away. With latency, each event is delivered in its own task, as in a browser. Under fake timers, an event due at the same moment as the one before it arrives a millisecond later, so advance a little past the latency, or use vi.advanceTimersToNextTimer().
How it behaves
- Events. Your code sees the events a browser would fire:
errorbefore aclosewith code1006, noopenafterclose(), and no messages afterclose().readyStatefollows the connection as your app sees it, so it'sCONNECTINGuntil the delayedopenarrives. - The server's side. Refused attempts never reach the server. When a connection drops, the real connection is closed with code
4999and reason'chaos', so your server frees it and its logs say why. A stalled connection stays open on the server until your app closes it, and then the server sees your close code, such as4000for a heartbeat timeout. - Closing a stalled connection. The close handshake can't finish, so the
closeevent has code1006, one task later. bufferedAmountincludes sends still held back bylatencyand anything sent into a stall, so it grows as it would on a real slow or dead connection.- It's not a
WebSocket.client.socket instanceof WebSocketisfalse. Everything else on theWebSocketinterface works, includingaddEventListener. - Browsers only, unless you say otherwise. With no
WebSocketoption, it wraps the globalWebSocketin browsers. On a server (nowindow) it throws when constructed, so the client fails the way it does during any server render. PassWebSocketexplicitly to use it in Node or a worker.
Limits
- TCP doesn't lose single messages. On an open connection, a WebSocket delivers every message, in order, or the connection dies.
lossstands for messages lost around a dead connection: sent into a socket that was already half-open, or published by the server while the client was reconnecting. Use it to test that your app notices gaps and resyncs, not to model packet loss. - No bandwidth limit. Latency is added per message, whatever its size.
- No hung handshake. A connection attempt either opens (after
latency) or is refused.
On a recording
Chaos wraps any WebSocket class, including a replay: createChaosWebSocket({ WebSocket: Replay }) adds faults that never happened in the recorded session.
See also
createChaosWebSocket: every option and method- Reconnecting and heartbeats
- Testing