Header Logo

How to Mock TTS and STT APIs in Django Unit Tests for Avatar Streaming Workflows

How to Mock TTS and STT APIs in Django Unit Tests for Avatar Streaming Workflows

Mock TTS/STT APIs in Django unit tests with clean boundaries for avatar streaming orchestration, transcripts, and session logic.

Introduction


When you build a Django service around realtime avatar workflows, your code usually touches two expensive, flaky dependencies: text-to-speech (TTS) and speech-to-text (STT). In production those services may stream audio, return partial transcripts, time out, or fail intermittently. In unit tests, you want the opposite: deterministic behavior, no network calls, and fast feedback.


This post shows how to mock TTS and STT APIs cleanly in Django tests so you can verify your orchestration logic without booting external services. By the end, you should be able to test avatar session creation, turn-taking logic, transcript handling, and error paths with predictable fixtures instead of brittle live integrations.


Test the orchestration, not the vendor


The first mistake is mocking too high or too low. If you mock every line of your view, you are not testing the control flow that matters. If you leave the real API calls in place, your tests become slow and nondeterministic.


The useful boundary is usually the client object or service wrapper that your Django code depends on. Your view, task, or domain service should call a small interface like tts_client.synthesize(...) or stt_client.transcribe(...). In tests, replace that interface with a fake or a unittest.mock.Mock.


# app/services/voice.py
# app/services/voice.py
# app/services/voice.py


That shape is easy to test because the service has explicit dependencies. If your code currently instantiates API clients inside the method, refactor first. Dependency injection pays for itself immediately in tests.


Mocking STT responses in Django unit tests


Speech-to-text tests usually need to cover three cases:


  1. Successful transcription returns the expected text.

  2. Partial or empty results are handled correctly.

  3. Failures propagate or degrade gracefully, depending on your API contract.


A common pattern is to patch the method that hits the network and return a stable string or object.


# tests/test_voice_pipeline.py
# tests/test_voice_pipeline.py
# tests/test_voice_pipeline.py


If your STT client returns structured data, model that structure in the mock rather than collapsing everything to a string. That keeps your tests aligned with the production contract. For example, if you care about confidence, language, or interim vs final segments, use a small fixture object or dataclass with those fields.


from dataclasses import dataclass
from dataclasses import dataclass
from dataclasses import dataclass


For error handling, make the mock raise the same exception class your wrapper would raise in production. That way you test your own retry, fallback, and logging behavior instead of some generic Exception path that never occurs in reality.


from app.clients import TranscriptionTimeout
from app.clients import TranscriptionTimeout
from app.clients import TranscriptionTimeout


Mocking TTS without pretending audio is text


TTS is where tests often become misleading. Production TTS usually returns bytes, a stream, or an audio URL. Your test should preserve that shape. Do not return a plain string unless your real client does.


If your application stores generated audio or sends it onward to a streaming layer, return bytes from the mock and assert that the right payload was requested.


def test_tts_payload_is_built_from_transcript(self):
def test_tts_payload_is_built_from_transcript(self):
def test_tts_payload_is_built_from_transcript(self):


If you need to verify the code that writes audio to a file, queue, or response body, keep that as a separate test. One unit test should validate the orchestration. Another should validate the persistence or transport layer. Splitting those concerns makes failures easier to interpret.


Use Django’s patching tools at the boundary


In Django tests, you can patch the import path where your code uses the client, not necessarily where the client is defined. That distinction matters. If your service module imports from app.clients import STTClient, patch app.services.voice.STTClient or patch the instance method on the object you inject.


from unittest.mock import patch
from unittest.mock import patch
from unittest.mock import patch


That said, if you find yourself assembling awkward types just to satisfy the patch, stop and move the creation of clients behind a factory or adapter. Tests should push you toward cleaner dependency edges, not preserve bad ones.


What to test in avatar streaming workflows


Realtime avatar systems add one more layer: the voice model, transcription layer, and avatar session are coordinated over streaming or event-driven APIs. In practice, your Django code usually does one of these:


  • Creates a session or issues credentials for a client-side embed.

  • Starts a backend workflow that receives transcript events and emits audio or control messages.

  • Routes voice-agent output into an avatar pipeline for synchronized playback.


For unit tests, focus on the deterministic decisions your app makes from those events:


  • Does a final transcript trigger a response?

  • Are empty or partial transcripts ignored?

  • Does a TTS failure mark the conversation failed or schedule a retry?

  • Does session creation persist the right metadata?


If your code uses Django signals, Celery tasks, or Channels consumers, the same principle applies: mock the external API boundary and assert the event handling behavior around it. Keep streaming concerns thin in unit tests; exercise actual streaming behavior separately in integration tests.


Where Protoface fits


This is exactly the kind of boundary that Protoface is meant to sit behind in a realtime avatar app. If your Django service is orchestrating a voice agent plus avatar session, mock the local wrapper that talks to the platform, not the platform itself. That keeps your unit tests fast while still letting you verify the data you send to the avatar workflow.


For example, if your app creates a realtime session through the REST API, the production code might look like this:


import requests
import requests
import requests


In your Django unit test, patch requests.post or, better, patch create_avatar_session itself and return a fixture dict. That way your test verifies the behavior of your app when a session is created, without depending on network calls or live credentials. The same approach works if your code uses the Python SDK or the LiveKit plugin; isolate those calls behind your own service boundary, then mock that boundary in tests. The platform docs at docs.protoface.com are the right place to confirm the exact request and response fields.


Practical gotchas


A few issues come up repeatedly:


  • Mocking the wrong import path. Patch the symbol where it is used, not where it is defined.

  • Returning the wrong shape. Match strings, bytes, dicts, or objects to the production contract.

  • Testing implementation details. Assert outcomes and important calls, not every intermediate line.

  • Letting async leak into sync tests. If you use async clients or consumers, use the appropriate async test case or event loop support.

  • Over-mocking streaming behavior. Unit tests should simulate the event contract, not reimplement WebRTC or media transport.


If your avatar workflow spans multiple services, a good split is: unit tests for your orchestration and mapping logic, a small number of integration tests for the transport boundary, and one or two end-to-end checks for the full realtime path.


Conclusion


Mocking TTS and STT in Django tests is mostly about clean boundaries. Wrap each external dependency behind a small interface, patch that interface in tests, and keep the mocked return values faithful to production shapes. For avatar streaming workflows, test the decisions your code makes from transcripts, audio payloads, and session events rather than trying to simulate the entire realtime stack in a unit test.


If you are wiring this into a Protoface-backed voice agent or avatar session, keep the platform call behind your own adapter and mock that adapter in Django. When you need the exact request fields or SDK usage, check the documentation, then add a small integration test separately. The result is a test suite that is fast, stable, and actually useful when something breaks.

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.