Header Logo

How to Test a Realtime Talking Avatar Integration in Django with Pytest and Mock WebSockets

How to Test a Realtime Talking Avatar Integration in Django with Pytest and Mock WebSockets

Test Django realtime avatar integrations with Pytest, mocked WebSockets, async consumers, and deterministic session state tests.

Introduction


Realtime avatar integrations fail in ways that ordinary request/response features do not. You are not just testing JSON serialization or authentication; you are testing a stream of events, latency-sensitive media coordination, and a state machine that spans HTTP, WebSocket, and often WebRTC. In Django, that usually means your test suite has to verify three things at once: the app can create and manage avatar/session state, the app reacts correctly to realtime events, and the integration fails cleanly when the network is flaky or messages arrive out of order.


This post shows a practical way to test that kind of integration with Pytest and mocked WebSockets. By the end, you should be able to:


  • isolate your Django code from the realtime transport,

  • simulate avatar/session lifecycle events deterministically,

  • assert that your app reacts correctly to streaming speech/avatar updates, and

  • keep the tests fast enough to run in CI.


What you should test, and what you should not


The common mistake is trying to “test the avatar” end-to-end in a unit test. That is usually the wrong layer. A realtime avatar integration has a few distinct concerns:


  • Application logic: does your Django view, service, or background task call the integration correctly?

  • Transport handling: does your code handle connect, reconnect, message dispatch, and close?

  • Protocol handling: does your code parse the messages you care about, and ignore the ones you do not?


For automated tests, focus on the first two. Mock the network boundary. You want confidence that your code reacts properly to the events you expect from a realtime avatar service, not confidence in the avatar vendor’s WebSocket stack.


A good rule: if a failure can be reproduced without a real socket, test it without a real socket. Reserve one or two manual or integration tests for the full path against a real service.


Model the integration as a small state machine


Realtime avatar flows typically look like this:


  1. Create or fetch an avatar/session over HTTP.

  2. Open a realtime channel.

  3. Send instructions or text/audio input.

  4. Receive events such as connected, speech started, media frames, speech ended, and error/close.

  5. Persist state or notify the rest of the app.


If you encode that as a state machine in your own code, your tests become simpler. For example, your Django service might track whether a session is “pending”, “active”, or “ended”, and store the latest avatar transcript or playback state. Your tests then assert on state transitions rather than on low-level socket internals.


from dataclasses import dataclass

self.status = "ended"
from dataclasses import dataclass

self.status = "ended"
from dataclasses import dataclass

self.status = "ended"


That model is intentionally boring. Boring is good. Once your app logic is isolated like this, the WebSocket test only needs to feed events into it.


Use Pytest fixtures to isolate Django from the realtime boundary


In Django, the best tests for this sort of code are usually service tests or view tests that patch the transport layer. If you are using Channels or an async consumer, the same principle applies: replace the real socket client with a fake object that behaves like the API you expect.


For a synchronous service that talks to a websocket client, a minimal fake can look like this:


class FakeWebSocket:

self.closed = True
class FakeWebSocket:

self.closed = True
class FakeWebSocket:

self.closed = True


Then patch your client factory or connector so the code under test receives the fake instead of a real socket. In Pytest, this is straightforward with monkeypatch or mocker:


import pytest

assert fake_ws.closed is True
import pytest

assert fake_ws.closed is True
import pytest

assert fake_ws.closed is True


The exact shape of your service will differ, but the pattern is stable: create a fake transport, feed deterministic events, assert on final state, and verify the right messages were sent.


Testing asynchronous consumers and reconnect logic


If your integration is async, use pytest-asyncio or Django’s async test support. The important part is still the same: do not depend on real network timing in unit tests.


For an async websocket client, make your fake awaitable and make reconnect behavior explicit. You want to test edge cases like:


  • the connection drops before the session becomes active,

  • an error event arrives after speech has started,

  • events are duplicated or reordered,

  • the consumer receives a close frame while work is still in flight.


A useful pattern is to separate “event ingestion” from “event handling”. The ingestion loop reads from the socket; the handler mutates state or emits side effects. That lets you unit test the handler directly and use a single mock WebSocket test for the loop itself.


import pytest

assert result.status in {"active", "retrying"}
import pytest

assert result.status in {"active", "retrying"}
import pytest

assert result.status in {"active", "retrying"}


Do not overfit this to one vendor’s event names. The practical goal is to verify your business rules around session state and retry policy. If the upstream service adds a new event type, your code should ignore it unless you have explicitly opted into handling it.


Mock the transport, but keep the payloads realistic


One trap with mocks is making them too fake. If your test messages are nothing like the real protocol payloads, the tests become decorative. The best mocks are thin: they remove the network, not the structure of the messages.


Keep payloads realistic in a few ways:


  • Use the same field names your production code expects.

  • Include the event ordering you see in practice.

  • Include empty or partial payloads for error-path tests.

  • Assert on message content, not on incidental implementation details.


For example, if your code sends a text prompt to start avatar speech, verify that the message contains the prompt and session identifier, but do not assert on every serialization detail unless that is actually part of your contract.


def test_sends_instruction_payload(monkeypatch):

assert fake_ws.sent[0]["instruction"] == "Speak clearly"
def test_sends_instruction_payload(monkeypatch):

assert fake_ws.sent[0]["instruction"] == "Speak clearly"
def test_sends_instruction_payload(monkeypatch):

assert fake_ws.sent[0]["instruction"] == "Speak clearly"


If you are testing code that wraps a vendor SDK rather than raw WebSockets, mock the SDK boundary instead of the socket. The same advice applies: keep the fake small and preserve the contract that matters to your app.


Where Protoface fits: test your app against the same contract you use in production


Protoface is useful here because it gives you a concrete realtime avatar surface to integrate against, whether you are creating sessions over the REST API, using the Python SDK, or dropping an avatar into a LiveKit voice agent with the plugin from the quickstarts. For testing, the main win is that your app can stay focused on its own contract: create session, connect, send input, consume events, update state.


If your production code uses the REST API to create a session, you can test the HTTP side separately with mocked responses, then test the realtime side with a fake WebSocket. A simple illustrative request looks like this:


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


And if you use the Python SDK, the same separation still helps: mock the SDK call that returns a session, then mock the socket-like object that delivers realtime events. The docs at docs.protoface.com are the right place to confirm the exact request and response fields before you hard-code them into tests.


Practical gotchas in CI


A few issues show up repeatedly when this runs in CI:


  • Async test leakage: make sure event loops are isolated and awaited tasks are cleaned up.

  • Flaky timing assertions: avoid sleeping and polling unless you absolutely must.

  • Over-mocking: if every layer is mocked independently, you can accidentally test a path that cannot occur in production.

  • Missing negative tests: validate error, disconnect, and timeout behavior, not just the happy path.


If your integration is especially complex, add one small contract test that runs against a real staging environment. Keep it separate from the unit suite so local runs stay fast. The unit suite should still catch the majority of regressions.


Conclusion


The core idea is simple: treat a realtime avatar integration like a transport-backed state machine, not like a single API call. In Django, that means keeping your business logic independent from the socket, then using Pytest and a mocked WebSocket to drive deterministic event sequences through that logic.


That approach gives you fast tests, better failure coverage, and less brittle code. When you are ready to wire it to a real service, verify the exact surface in the docs and keep the integration boundary narrow. Start with docs.protoface.com, then build one clean contract around session creation, realtime events, and state updates.

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.