← Back to blog

Testing Real-Time React Components: Strategies for Trading UIs

How to write reliable tests for components that depend on WebSocket data — mocking feeds, simulating rapid updates, and testing reconnection edge cases.


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:

  1. Capture WebSocket messages during a volatile market period
  2. Save as a JSON array with timestamps
  3. 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.

Kickstart Your Trading Application

Hedge UI is a React starter kit with production-ready trading components, real-time data handling, and customisable layouts — so you can ship faster.

Get Hedge UI