Skip to content

Testing Guide

Run the deterministic suite before committing. It blocks real HTTP transports and checks requests, response models, retries, streaming cleanup and documentation tools.

# Unit tests
uv run poe test

# SDK line and branch coverage, with the same 99% gate used by CI
uv run poe test-cov

# A specific contract suite
uv run pytest tests/unit/test_client_transport.py

# Quality checks
uv run poe check-fast

# Execute Markdown Python examples with the real SDK and offline HTTP fixtures
uv run poe docs-validate-examples

# Execute all six notebooks through HTTP mocks
uv run poe docs-validate-notebooks

An ordinary uv run pytest includes unit tests and skips the live integration suite. CI runs unit tests on Python 3.10, 3.11, 3.12 and 3.13. A separate job checks the minimum supported HTTPX and Pydantic versions on Python 3.10.

Test structure

tests/
├── conftest.py                   # Live-test opt-in
├── unit/
│   ├── conftest.py               # Reject unmocked HTTP
│   ├── test_client_transport.py  # HTTP, retries and logging
│   ├── test_sync_client.py       # Background loop and stream lifecycle
│   ├── test_request_contracts.py
│   ├── test_response_contracts.py
│   └── models/                  # Model fields and wire serialization
└── integration/
    ├── conftest.py               # Practice fixtures and verified cleanup
    ├── test_account_reads.py
    ├── test_api_errors.py
    ├── test_market_data.py
    ├── test_transaction_reads.py
    └── test_trading_lifecycle.py

Write tests at the HTTP boundary

Use httpx.MockTransport with an independent wire payload. Assert the outgoing request and observable response values, and close every client explicitly.

import httpx

from fivetwenty import AsyncClient


async def test_account_discovery() -> None:
    def respond(request: httpx.Request) -> httpx.Response:
        assert request.method == "GET"  # noqa: S101
        assert request.url.path == "/v3/accounts"  # noqa: S101
        return httpx.Response(200, json={"accounts": [{"id": "123", "tags": []}]})

    transport = httpx.AsyncClient(
        transport=httpx.MockTransport(respond),
        base_url="https://offline.example.test/v3",
    )
    async with AsyncClient(token="offline-token", account_id="123", transport=transport) as client:
        accounts = await client.accounts.get_accounts()
    assert [account.id for account in accounts] == ["123"]  # noqa: S101

Pytest's configured asyncio mode runs asynchronous test functions automatically. Use Decimal and Python datetimes for value assertions. Control clocks, jitter and transport failures so that tests do not depend on random delays or network speed. Consume streaming generators and verify response cleanup when closing or cancelling.

Keep enum wire-value assertions and independent model examples. Generated model round trips provide broad regression coverage, but cannot establish API fidelity by themselves. Avoid duplicate presence checks, always-true assertions, or exception handlers that merely print a failure.

Live integration

Live tests require explicit opt-in and OANDA practice credentials:

export FIVETWENTY_OANDA_TOKEN="your-practice-token"
export FIVETWENTY_OANDA_ACCOUNT="your-practice-account"

# Read-only server checks
uv run pytest tests/integration --run-integration-live -m 'not trading'

# Includes order and trade operations
uv run poe test-integration

Use a dedicated practice account with no other process trading on it. Trading tests require no pending orders or open trades before starting, use the instrument's minimum trade size, and verify cleanup after success or failure. Failed cleanup fails the test. Known market-state restrictions can skip a case; unexpected errors must propagate. SKIP_INTEGRATION=1 forces all live cases to skip.

The live suite checks focused server outcomes. Regional order restrictions and transport failure matrices also need deterministic offline tests; a passing live run does not verify every account-specific API feature.

Coverage interpretation

The SDK gate measures fivetwenty with branch coverage. Model and enum declarations contribute to the percentage, so review assertion quality alongside that number. Do not add source exclusions or weak tests to increase coverage.

Documentation tooling is a separate measurement:

uv run pytest tests/unit --cov=docs_validation.src --cov-branch --cov-report=term-missing --cov-fail-under=0

Actual documentation diagnostics are tracked separately from tests of the validators. The Markdown execution worker runs in a subprocess, so its execution is not included in this in-process coverage measurement. Runner regressions exercise valid examples, broken SDK calls, invalid responses, environment isolation and worker failures.