Testing a trading UI is different from testing a typical web application. Your components depend on data that arrives asynchronously at unpredictable rates, changes state rapidly, and can disconnect without warning. Standard React Testing Library patterns need adaptation.
The testing challenge
A trading component like an order book has several properties that make it hard to test:
- It receives data from a WebSocket, not from props or API calls
- The data changes many times per second
- The component's correctness depends on the order of updates, not just their content
- Edge cases (disconnection, stale data, empty books) are critical but hard to reproduce
Mocking WebSocket connections
Do not use real WebSocket connections in tests. Instead, create a mock that lets you control exactly when and what messages arrive:
class MockWebSocket {
onmessage: ((event: MessageEvent) => void) | null = null;
onclose: (() => void) | null = null;
onopen: (() => void) | null = null;
readyState = WebSocket.OPEN;
constructor(public url: string) {
setTimeout(() => this.onopen?.(), 0);
}
simulateMessage(data: unknown) {
this.onmessage?.(new MessageEvent("message", {
data: JSON.stringify(data),
}));
}
simulateClose() {
this.readyState = WebSocket.CLOSED;
this.onclose?.();
}
close() {
this.simulateClose();
}
}
Inject this mock via dependency injection or by replacing the global WebSocket constructor in your test setup:
let mockWs: MockWebSocket;
beforeEach(() => {
vi.stubGlobal("WebSocket", class extends MockWebSocket {
constructor(url: string) {
super(url);
mockWs = this;
}
});
});
Testing data rendering
With the mock in place, you can test that components render data correctly:
it("displays bid and ask prices from WebSocket updates", async () => {
render(<OrderBook symbol="BTC-USD" />);
// Simulate order book snapshot
mockWs.simulateMessage({
type: "snapshot",
bids: [{ price: 50000, quantity: 1.5 }],
asks: [{ price: 50001, quantity: 2.0 }],
});
await waitFor(() => {
expect(screen.getByText("50,000.00")).toBeInTheDocument();
expect(screen.getByText("50,001.00")).toBeInTheDocument();
});
});
Testing rapid updates
Trading components must handle bursts of updates without visual glitches or stale data. Test this by sending multiple updates in quick succession:
it("correctly applies rapid sequential updates", async () => {
render(<OrderBook symbol="BTC-USD" />);
// Send snapshot
mockWs.simulateMessage({
type: "snapshot",
bids: [{ price: 50000, quantity: 1.0 }],
asks: [{ price: 50001, quantity: 1.0 }],
});
// Rapidly update the same price level
for (let i = 0; i < 100; i++) {
mockWs.simulateMessage({
type: "delta",
bids: [{ price: 50000, quantity: 1.0 + i * 0.1 }],
asks: [],
});
}
// After flushing, should show the final value
await waitFor(() => {
expect(screen.getByText("10.90")).toBeInTheDocument(); // Final quantity
});
});
If your component uses a buffered flush strategy (which it should), you may need to advance timers:
vi.useFakeTimers();
// ... send updates ...
vi.advanceTimersByTime(100); // Trigger the flush interval
await waitFor(() => {
expect(screen.getByText("10.90")).toBeInTheDocument();
});
Testing disconnection and reconnection
Traders need to know when their data feed is stale. Test that your component surfaces connection state:
it("shows a stale data indicator on disconnection", async () => {
render(<OrderBook symbol="BTC-USD" />);
mockWs.simulateMessage({
type: "snapshot",
bids: [{ price: 50000, quantity: 1.0 }],
asks: [{ price: 50001, quantity: 1.0 }],
});
// Simulate disconnection
mockWs.simulateClose();
await waitFor(() => {
expect(screen.getByText("Reconnecting")).toBeInTheDocument();
});
});
Testing order state transitions
Order components are state machines. Test each valid transition:
it("transitions from pending to partial fill", async () => {
render(<OrderRow order={pendingOrder} />);
expect(screen.getByText("Pending")).toBeInTheDocument();
// Simulate acknowledgement
rerender(<OrderRow order={{ ...pendingOrder, status: "acknowledged", exchangeOrderId: "123" }} />);
expect(screen.getByText("Working")).toBeInTheDocument();
// Simulate partial fill
rerender(<OrderRow order={{
...pendingOrder,
status: "partial",
exchangeOrderId: "123",
filledQuantity: 0.5,
avgFillPrice: 50000,
}} />);
expect(screen.getByText("0.50 / 1.00")).toBeInTheDocument();
});
Snapshot testing for number formatting
Number formatting bugs are common and hard to catch visually. Snapshot tests catch regressions:
it.each([
[0.00000001, "0.00000001"],
[1234.56, "1,234.56"],
[0.1 + 0.2, "0.30"], // Floating point edge case
[1000000, "1,000,000.00"],
[-500.5, "-500.50"],
])("formats price %f as %s", (input, expected) => {
const instrument = { pricePrecision: 2 } as Instrument;
expect(formatPrice(input, instrument)).toBe(expected);
});
Integration testing with recorded data
For the most realistic tests, record actual WebSocket sessions and replay them:
- Capture WebSocket messages during a volatile market period
- Save as a JSON array with timestamps
- Replay in tests with simulated timing
import recordedSession from "./fixtures/btc-crash-2024-01-15.json";
it("handles a real market crash scenario", async () => {
render(<TradingView symbol="BTC-USD" />);
for (const message of recordedSession) {
mockWs.simulateMessage(message.data);
vi.advanceTimersByTime(message.delayMs);
}
// Verify the UI is responsive and showing correct final state
expect(screen.getByTestId("bid-price")).toHaveTextContent("42,150.00");
});
This is the closest you can get to testing production conditions without a live exchange connection.
Test organisation
Structure your trading component tests by behaviour, not by component method:
__tests__/
order-book/
rendering.test.tsx # Does it display data correctly?
updates.test.tsx # Does it handle rapid updates?
reconnection.test.tsx # Does it recover from disconnections?
edge-cases.test.tsx # Empty book, single level, price crossing
order-form/
validation.test.tsx # Input constraints
submission.test.tsx # Order placement flow
keyboard.test.tsx # Keyboard shortcut interactions
Each test file focuses on one aspect of behaviour, making failures immediately informative about what broke.