VoiceGateway // DOCS

Testing

VoiceGateway has 200+ tests with over 70% code coverage. This guide covers running tests, writing new ones, and using the shared fixtures.

Testing

VoiceGateway has 200+ tests with over 70% code coverage. This guide covers running tests, writing new ones, and using the shared fixtures.

Running tests

Shell
# Run all tests
pytest

# Run a specific file
pytest src/voicegateway/tests/core/test_config.py

# Run a specific test by name
pytest src/voicegateway/tests/core/test_config.py::test_load_example_config

# Run with coverage
pytest --cov

# Run with coverage and show missing lines
pytest --cov --cov-report=term-missing

# Run verbose (see each test name)
pytest -v

# Stop at first failure
pytest -x

pytest configuration

VoiceGateway uses asyncio_mode = "auto" in pyproject.toml:

TOML
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

This means:

  • No @pytest.mark.asyncio needed -- async test functions are detected automatically
  • All tests run in the same event loop policy -- no loop conflicts
  • Test files live in the src/voicegateway/tests/ directory

Shared fixtures

The src/voicegateway/tests/conftest.py file provides four key fixtures:

_test_env (autouse)

Runs automatically for every test. Sets fake API keys so provider constructors do not fail:

Python
@pytest.fixture(autouse=True)
def _test_env(monkeypatch):
    for key in [
        "OPENAI_API_KEY",
        "DEEPGRAM_API_KEY",
        "CARTESIA_API_KEY",
        "ANTHROPIC_API_KEY",
        "GROQ_API_KEY",
        "ELEVENLABS_API_KEY",
        "ASSEMBLYAI_API_KEY",
    ]:
        monkeypatch.setenv(key, "test-key-value")

You never need to call this fixture explicitly -- it is autouse.

example_config_path

Writes the bundled starter config (sourced from src/voicegateway/data/voicegw.example.yaml) to a tmp file and returns its path. Use this to test config loading against the shipped example:

Python
def test_load_example(example_config_path):
    config = GatewayConfig.load(example_config_path)
    assert config.providers

temp_config

Writes a minimal voicegw.yaml to a temporary directory and returns its path. The config includes OpenAI and Deepgram providers, one STT model, one LLM model, two projects (test-project and blocked-project), and cost tracking enabled:

Python
def test_gateway_init(temp_config):
    gw = Gateway(config_path=temp_config)
    assert gw is not None

seeded_storage

Creates a SQLiteStorage instance pre-loaded with three sample RequestRecord entries:

RecordModalityModelProjectCost
1sttdeepgram/nova-3test-project$0.0043
2llmopenai/gpt-4o-minitest-project$0.015
3llmopenai/gpt-4o-minidefault$0.008
Python
async def test_query_costs(seeded_storage):
    costs = await seeded_storage.get_costs(project="test-project")
    assert len(costs) == 2

Writing tests

Test file naming

  • Test files: src/voicegateway/tests/test_<module>.py
  • Test functions: test_<what_it_tests>
  • Test classes (grouping related tests): TestClassName

Async tests

Write async tests as regular async def functions. The asyncio_mode = "auto" setting handles the rest:

Python
async def test_health_check():
    provider = OpenAIProvider({"api_key": "test-key"})
    # Mock the HTTP call
    result = await provider.health_check()
    assert result is True

Mocking providers

Providers make HTTP calls to external APIs. Always mock these in tests:

Python
from unittest.mock import AsyncMock, patch


async def test_stt_fallback():
    with patch(
        "voicegateway.providers.deepgram_provider.DeepgramProvider.health_check",
        new_callable=AsyncMock,
        return_value=False,
    ):
        # Deepgram is "down", fallback should kick in
        ...

Mocking the config

Use temp_config for tests that need a Gateway instance, or construct configs directly:

Python
def test_router_resolution(temp_config):
    config = GatewayConfig.load(temp_config)
    router = Router(config)
    provider, model = router.resolve("openai/gpt-4o-mini", "llm")
    assert model == "gpt-4o-mini"

Testing cost calculations

Python
from decimal import Decimal

from voicegateway.pricing import catalog


def test_deepgram_nova3_pricing():
    """One minute of Deepgram Nova-3 STT prices at $0.0043 via the catalog."""
    cost = catalog.calculate_cost("stt", "deepgram/nova-3", audio_seconds=60)
    assert cost == Decimal("0.0043")

Testing middleware

Middleware wraps provider calls. Test the wrapping behavior:

Python
async def test_budget_enforcer_blocks():
    """Budget enforcer should raise when project exceeds daily budget."""
    enforcer = BudgetEnforcer(storage=seeded_storage, config=config)
    with pytest.raises(BudgetExceededError):
        await enforcer.check("blocked-project")

Mock patterns

monkeypatch.setenv for environment variables

Python
def test_custom_db_path(monkeypatch, tmp_path):
    db_path = str(tmp_path / "custom.db")
    monkeypatch.setenv("VOICEGW_DB_PATH", db_path)
    gw = Gateway(config_path=temp_config)
    assert gw._storage is not None

tmp_path for temporary files

pytest's built-in tmp_path fixture provides a temporary directory unique to each test:

Python
async def test_sqlite_storage(tmp_path):
    storage = SQLiteStorage(str(tmp_path / "test.db"))
    await storage.log_request(record)

patch for external HTTP calls

Python
from unittest.mock import patch, MagicMock

def test_provider_creation():
    with patch("voicegateway.providers.openai_provider.openai") as mock_openai:
        provider = OpenAIProvider({"api_key": "test"})
        llm = provider.create_llm("gpt-4o-mini")
        assert llm is not None

Coverage expectations

  • New features must include tests
  • Bug fixes should include a regression test
  • Target: maintain above 70% overall coverage
  • Critical paths (Gateway, Router, CostTracker, BudgetEnforcer) should be above 90%

Check coverage for specific modules:

Shell
pytest --cov=voicegateway.core --cov-report=term-missing
pytest --cov=voicegateway.middleware --cov-report=term-missing

On this page