Testing Toolkit

Updated Oct 15, 2026
  • WebSocketTestClient – Real websocket client powered by websockets, designed for integration tests; captures original payloads for assertions, while trace representations and TraceEvent.summary() show safe metadata.
  • WebSocketSimulator – In-memory fake implementing the WebSocket protocol for fast unit tests.
  • Live server harness – LiveWebSocketServer serves a real Falcon ASGI app on an ephemeral localhost port. The live_websocket_server pytest fixture provides start(app) and a loop-bound run(awaitable, timeout=...) helper. Connect with server.connect(path) to use WebSocketTestClient over a real RFC WebSocket connection and have the fixture track the client for teardown.
  • Pytest fixtures – See tests/behaviour/*.feature and falcon_pachinko/unittests helpers for factory utilities.

An async test can use the server directly:

import pytest

from falcon_pachinko.testing import LiveWebSocketServer


@pytest.mark.asyncio
async def test_ping_reply() -> None:
    app = build_app()

    async with LiveWebSocketServer(app) as server:
        async with server.connect("/ws/chat") as session:
            await session.send_json({"type": "ping"})
            assert await session.receive_json() == {"type": "pong"}

        assert server.pop_errors() == ()

Entering the server waits for ASGI lifespan startup and listener readiness. server.connect() tracks each client, and server teardown closes any clients the test leaves open. server.errors returns a read-only snapshot of captured failures; server.pop_errors() returns and acknowledges them. The harness captures exceptions handled by Falcon, exceptions escaping the ASGI callable, and server-task failures. Unacknowledged failures are grouped in LiveServerError when the test body completed without an exception. If the test body is already raising, the harness adds captured failures as notes to that exception so the original failure stays primary.

When capture_errors=True, use a fresh Falcon app for each server instance: the capture handler remains registered on the app after server shutdown.

A startup timeout or ordinary startup exception raises LiveServerStartupError. A shutdown deadline is recorded as LiveServerShutdownError and reported during teardown using the same error handling. The harness installs its Falcon Exception capture handler when the server starts, replacing a handler registered earlier. If the application needs a custom handler, register it after startup and have it call server.record_error(ex); otherwise Falcon may consume the exception before the harness can report it. For synchronous tests, use the live_websocket_server fixture's start(app) and run(awaitable, timeout=...) methods instead. See the developer guide's WebSocket lifecycle guidance for the fixture and custom-handler details.

The websocket_simulator pytest fixture (from falcon_pachinko.testing) takes no parameters and yields a SimulatorRouterHarness: a pre-mounted router and app wired to inject a WebSocketSimulator into each connection. Register routes on harness.router, then dispatch a connection with async with harness.connect(path) as connection: .... On teardown the fixture calls harness.discard_pending_simulator(), discarding any simulator staged for a connection that was never established, so tests do not need to assemble a router and simulator by hand.

Recommended strategy:

  • Unit test pure resource logic with WebSocketSimulator.
  • Use pytest-bdd live-server scenarios as the required acceptance path for every WebSocket lifecycle feature. These prove that real Falcon ASGI responders keep a socket open and exchange frames over a real connection.
  • Keep simulator tests for fast routing, hooks, DI, and connection-manager coverage; they complement the live suite and do not replace it.
  • The lifespan_workers.feature BDD scenarios verify workers running during application lifespan; the feature file is not a fixture.