Testing¶
VelociPy ships with TestClient and AsyncTestClient so you can hunt bugs
without starting a server. Both clients drive a VelociPy app directly and
support the same ASGI and RSGI interfaces you run in production.
No extra framework dependencies are required. The clients work with any test
runner that supports synchronous or asynchronous tests, such as pytest with
pytest-asyncio.
Run the examples
See examples/testing.py for a runnable demo of sync, async, ASGI/RSGI, header, cookie, QUERY, and manual-cleanup tests.
Synchronous tests¶
Use TestClient as a context manager from ordinary synchronous test code:
from velocipy import VelociPy
from velocipy.status import HTTP_200_OK
from velocipy.testing import TestClient
app = VelociPy()
@app.get("/")
def index() -> dict[str, str]:
return {"hello": "world"}
def test_index() -> None:
with TestClient(app) as client:
response = client.get("/")
assert response.status_code == HTTP_200_OK
assert response.json() == {"hello": "world"}
The context manager enters the app's lifespan if one is configured, so startup and shutdown hooks run around the test.
Asynchronous tests¶
For async test suites, use AsyncTestClient with async with:
import pytest
from velocipy import VelociPy
from velocipy.status import HTTP_200_OK
from velocipy.testing import AsyncTestClient
app = VelociPy()
@app.get("/")
def index() -> dict[str, str]:
return {"hello": "world"}
@pytest.mark.asyncio
async def test_index_async() -> None:
async with AsyncTestClient(app) as client:
response = await client.get("/")
assert response.status_code == HTTP_200_OK
assert response.json() == {"hello": "world"}
Testing both interfaces¶
VelociPy runs on ASGI (Uvicorn, Hypercorn) and RSGI (Granian). Test both
interfaces with pytest.mark.parametrize:
import pytest
from velocipy import VelociPy
from velocipy.constants import Interface
from velocipy.status import HTTP_200_OK
from velocipy.testing import TestClient
app = VelociPy()
@app.get("/")
def index() -> dict[str, str]:
return {"hello": "world"}
@pytest.mark.parametrize("interface", [Interface.ASGI, Interface.RSGI])
def test_index_on_both_interfaces(interface: Interface) -> None:
with TestClient(app, interface=interface) as client:
response = client.get("/")
assert response.status_code == HTTP_200_OK
assert response.json() == {"hello": "world"}
RSGI is the fastest production path; ASGI offers the broadest compatibility with existing async middleware and tools. Testing both catches interface-specific surprises early.
Headers and cookies¶
The clients accept the same arguments as httpx. Pass headers directly and
pass cookies through the client constructor:
from typing import Annotated
from velocipy import Cookie, Header, VelociPy
from velocipy.status import HTTP_200_OK
from velocipy.testing import TestClient
app = VelociPy()
@app.get("/headers")
def headers(x_token: Annotated[str, Header(...)]) -> dict[str, str]:
return {"token": x_token}
@app.get("/cookies")
def cookies(session_id: Annotated[str, Cookie(...)]) -> dict[str, str]:
return {"session_id": session_id}
def test_headers() -> None:
with TestClient(app) as client:
response = client.get("/headers", headers={"x-token": "secret"})
assert response.status_code == HTTP_200_OK
assert response.json() == {"token": "secret"}
def test_cookies() -> None:
with TestClient(app, cookies={"session_id": "abc123"}) as client:
response = client.get("/cookies")
assert response.status_code == HTTP_200_OK
assert response.json() == {"session_id": "abc123"}
The QUERY method¶
VelociPy supports the HTTP QUERY method. Use client.query(...) to test
QUERY endpoints:
from typing import Any
from velocipy import VelociPy
from velocipy.status import HTTP_200_OK
from velocipy.testing import TestClient
app = VelociPy()
@app.query("/search")
def do_search(query: dict[str, Any]) -> dict[str, Any]:
return {"q": query.get("q"), "limit": query.get("limit", 10)}
def test_query_method() -> None:
with TestClient(app) as client:
response = client.query(
"/search",
json={"q": "velocipy", "limit": 5},
)
assert response.status_code == HTTP_200_OK
assert response.json() == {"q": "velocipy", "limit": 5}
Lifespan events¶
If the app defines a lifespan context manager, the test clients enter it when
the context opens and exit it on close. This lets you test startup resources
such as database pools or configuration loading:
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from typing import Any
from velocipy import VelociPy
from velocipy.testing import TestClient
@asynccontextmanager
async def lifespan(app: VelociPy) -> AsyncIterator[None]:
app.state.started = True
yield
app.state.started = False
app = VelociPy(lifespan=lifespan)
@app.get("/")
def index() -> dict[str, bool]:
return {"started": app.state.started}
def test_lifespan() -> None:
with TestClient(app) as client:
response = client.get("/")
assert response.json() == {"started": True}
Manual cleanup¶
When a context manager is inconvenient, create the client directly and close it explicitly:
from velocipy.testing import TestClient
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
client.close()
For async clients, await aclose():
async def test_with_manual_cleanup() -> None:
client = AsyncTestClient(app)
response = await client.get("/")
assert response.status_code == 200
await client.aclose()
Dependency overrides¶
Replace dependencies during tests with app.dependency_overrides:
from typing import Annotated, Any
from velocipy import Depends, VelociPy
from velocipy.testing import TestClient
app = VelociPy()
def real_db() -> dict[str, Any]:
return {"source": "production"}
@app.get("/items")
async def list_items(db: Annotated[dict[str, Any], Depends(real_db)]):
return db
def test_with_override() -> None:
app.dependency_overrides[real_db] = lambda: {"source": "test"}
try:
client = TestClient(app)
response = client.get("/items")
assert response.json() == {"source": "test"}
finally:
app.dependency_overrides.clear()
See Dependencies for more on dependency injection and overrides.
Custom base URL¶
The default base URL is http://testserver. Override it when your handlers
inspect the request URL:
Testing uploads, WebSockets, and background tasks¶
Feature-specific testing patterns are covered on their own pages:
- Uploads — multipart file uploads with
files={...}. - Background tasks — mocking task functions because they run after the response.
- WebSockets — WebSocket endpoint testing.
At a glance¶
| Pattern | How |
|---|---|
| Sync test | with TestClient(app) as client: |
| Async test | async with AsyncTestClient(app) as client: |
| ASGI/RSGI | TestClient(app, interface=Interface.RSGI) |
| Headers | client.get("/", headers={...}) |
| Cookies | Pass cookies={...} to the client constructor |
| QUERY | client.query("/search", json={...}) |
| Lifespan | Entered by the context manager automatically |
| Cleanup | client.close() / await client.aclose() |
| Override | app.dependency_overrides[func] = replacement |
| Base URL | TestClient(app, base_url="...") |
Related examples¶
| Example | What it shows |
|---|---|
examples/testing.py |
Sync, async, ASGI/RSGI, header, cookie, QUERY, and manual-cleanup tests. |
Related documentation¶
- Getting started — install VelociPy and build your first app.
- Dependencies — dependency injection and overrides.
- Uploads — testing multipart file uploads.
- Background tasks — testing deferred work.
- WebSockets — testing WebSocket endpoints.