> ## Documentation Index
> Fetch the complete documentation index at: https://docs.voicegateway.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing

# 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

```bash theme={null}
# 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 theme={null}
[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 theme={null}
@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 theme={null}
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 theme={null}
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:

| Record | Modality | Model              | Project      | Cost     |
| ------ | -------- | ------------------ | ------------ | -------- |
| 1      | stt      | deepgram/nova-3    | test-project | \$0.0043 |
| 2      | llm      | openai/gpt-4o-mini | test-project | \$0.015  |
| 3      | llm      | openai/gpt-4o-mini | default      | \$0.008  |

```python theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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 theme={null}
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:

```bash theme={null}
pytest --cov=voicegateway.core --cov-report=term-missing
pytest --cov=voicegateway.middleware --cov-report=term-missing
```

## Related pages

* [Development Setup](/contributing/development-setup)
* [Code Style](/contributing/code-style)
* [Adding a Provider](/contributing/adding-a-provider)
* [Contributing](/contributing/)
