Header Logo

How to Test WebSocket Signaling for Realtime AI Avatars in Python with pytest

How to Test WebSocket Signaling for Realtime AI Avatars in Python with pytest

Test WebSocket signaling for realtime AI avatars in Python with pytest: order, state, errors, timeouts, reconnects.

Introduction


If you are building realtime avatars, the part that usually breaks first is not rendering or transcription; it is signaling. The WebSocket handshake that coordinates session setup, media parameters, and control events is where client/server assumptions diverge, retries get messy, and race conditions hide. For Python teams, Protoface is one example of a system where that signaling path matters: you may be creating sessions over REST, then driving a realtime avatar through a streaming connection while your voice agent is already live.


This post shows how to test WebSocket signaling for a realtime avatar workflow with pytest. By the end, you should be able to write deterministic tests for connection establishment, message sequencing, error handling, and reconnect behavior without needing a browser or a full end-to-end media stack.


What exactly you should test


For realtime avatars, WebSocket signaling is the control plane, not the media plane. Audio/video may flow over WebRTC or a similar transport, but the WebSocket usually carries session negotiation, state updates, acknowledgements, and out-of-band events such as readiness, transcript updates, or avatar state changes.


The high-value tests are the ones that assert protocol behavior, not implementation details:


  • Handshake succeeds with the right auth and session metadata.

  • Client sends messages in the expected order.

  • Server responds with the correct event types and correlation IDs.

  • Invalid payloads produce deterministic errors.

  • Disconnects and reconnects do not corrupt session state.


A good rule is: if the bug would show up as a broken avatar session in production, you want a test for the signaling path. If it is purely visual polish, keep it in a higher-level integration test.


Build the smallest useful test harness


The easiest way to test a WebSocket protocol in Python is to keep the server side in-process and use a real WebSocket client against it. That gives you realistic framing, async behavior, and message ordering, but still lets you assert on every event.


For async code, pytest plus pytest-asyncio is usually enough. A simple server fixture can expose a handler that speaks your signaling protocol. In production, the same handler might back your avatar session orchestration layer.


import json

assert reply == {"type": "ready", "session_id": "sess_123"}
import json

assert reply == {"type": "ready", "session_id": "sess_123"}
import json

assert reply == {"type": "ready", "session_id": "sess_123"}


This is intentionally small. The point is not to simulate every edge of your backend; it is to make protocol behavior observable and reproducible.


Test message sequencing and state transitions


Realtime systems fail when messages arrive in the wrong order or when a client assumes the server has already advanced state. For avatars, a common pattern is:


  1. client connects

  2. client announces session intent

  3. server validates and sends ready

  4. client begins sending control events or subscribes to downstream updates


Your tests should explicitly verify ordering. If you care about state transitions, encode them in the protocol and assert on them. Don’t infer state from side effects.


import json

assert received == ["join", "avatar.control"]
import json

assert received == ["join", "avatar.control"]
import json

assert received == ["join", "avatar.control"]


That last assertion is useful because it catches accidental protocol drift. If someone changes the client to emit control messages before the session is ready, the test fails immediately.


Test failure paths, timeouts, and reconnects


Most production bugs in signaling are not happy-path bugs. They are timeouts, partial sends, stale session IDs, and duplicate events after reconnect.


For those, build tests that simulate the exact failure mode. A few examples:


  • Server never sends ready; client times out and surfaces a clear error.

  • Client sends malformed JSON; server closes with an application-specific code.

  • Client reconnects and reuses an old session ID; server rejects or resumes deterministically.

  • Server emits duplicate events; client deduplicates using an event ID or monotonic sequence.


With async tests, use short explicit timeouts so failures are fast and readable. Do not rely on default waits; they make CI flaky.


import asyncio

await asyncio.wait_for(ws.recv(), timeout=0.1)
import asyncio

await asyncio.wait_for(ws.recv(), timeout=0.1)
import asyncio

await asyncio.wait_for(ws.recv(), timeout=0.1)


If your protocol includes reconnect semantics, test them explicitly instead of assuming the underlying WebSocket library handles them. Libraries reconnect the socket; they do not restore your application state unless you implement that logic.


Use mocks sparingly; prefer protocol-level assertions


It is tempting to mock the WebSocket object and assert that send() was called with certain payloads. That can be useful for very small units, but it tends to miss the bugs that matter: framing, ordering, timing, and disconnect handling.


A better layering is:


  • Unit tests for pure message serialization/deserialization functions.

  • Protocol tests against a real local WebSocket server.

  • End-to-end tests only for the full avatar experience.


That keeps the test suite fast while still catching the kinds of regressions that would break a live avatar session.


How Protoface fits into this


For a developer platform like Protoface, the WebSocket layer is typically one part of a broader session lifecycle: you create or manage an avatar session through the REST API, then the realtime client connects and exchanges signaling messages that drive the avatar experience. In practice, that means your tests should cover both the HTTP setup step and the WebSocket follow-through.


The REST side is straightforward to smoke-test with curl or Python, using an API key in the Authorization header. Exact request fields depend on the endpoint, but the shape is the same: create a session, inspect the response, then connect your client with the returned session data.


curl -X POST https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123"}'
curl -X POST https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123"}'
curl -X POST https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123"}'


For LiveKit-based voice agents, the same testing approach applies at the plugin boundary: verify that your agent joins the session, the avatar becomes ready, and control events move through the expected states. If you are using the Python SDK or the LiveKit plugin, keep the protocol tests close to the code that translates your agent state into avatar signaling. The docs are the right place for the exact request and event schemas, and the plugin or SDK repos are useful references for integration patterns.


Practical pytest patterns that help in CI


A few habits make these tests reliable in continuous integration:


  • Bind servers to 127.0.0.1 and a random free port.

  • Use one test per behavior, not one giant flow test.

  • Keep timeouts short and explicit.

  • Assert on payload contents, not only “message received.”

  • Close sockets cleanly so tests do not leak background tasks.


If you need reusable setup, create fixtures for the server, the client, and any protocol codec. That makes it easier to evolve the protocol without rewriting every test.


One useful fixture pattern is to separate transport from business logic. Your handler can accept a callable that implements session behavior, which makes it easy to test edge cases by swapping in different behaviors.


Conclusion


Testing WebSocket signaling for realtime AI avatars is mostly about making the protocol explicit. Once you assert on message order, state transitions, error handling, and reconnect behavior, the rest of the stack becomes much easier to reason about. The key is to test against a real WebSocket connection with pytest, not a hand-wavy mock, and to keep HTTP session creation and realtime signaling covered together.


If you are integrating a realtime avatar workflow, start with the protocol tests first, then add one thin end-to-end test around the complete session lifecycle. For the concrete API shapes, SDK usage, and integration examples, check docs.protoface.com and the relevant GitHub examples.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.