Testing
Test behavior at the narrowest useful boundary: use cases directly for application logic, the typed client for contract behavior, and raw HTTP for exact envelopes and headers.
Test use cases directly
async def test_create_todo_persists() -> None:
repository = MemoryTodoRepository()
context = AppContext(todos=repository)
todo = await create_todo(CreateTodo(title="Buy milk"), context)
assert todo.completed is False
assert await repository.get(todo.id) == todoThese tests need no ASGI application and make authorization and domain behavior easy to exercise.
Use the typed in-process client
from tenchi.testing import open_client
async with open_client(app) as client:
created = await client.call_with_response(
create_todo_contract,
request=CreateTodo(title="Buy milk"),
)
assert created.body.title == "Buy milk"
assert created.headers.location.endswith(created.body.id)open_client() runs the application lifespan and uses httpx's ASGI transport.
It provides the same contract validation as a network client.
Pass observers= to collect the same payload-safe ClientOutcome values that
a deployed client emits.
Assert raw HTTP behavior
from tenchi.testing import open_http
async with open_http(app) as http:
response = await http.post(
"/todos",
json={"title": ""},
)
assert response.status_code == 422
assert response.headers["x-tenchi-error-source"] == "framework"
assert response.json()["code"] == "VALIDATION_ERROR"Use raw HTTP for malformed input, media types, request IDs, middleware, application error envelopes, and other wire-level behavior.
Replace ports, not framework internals
Build a fresh application around memory adapters for behavior-focused integration tests. Use temporary real adapters when lifecycle, transactions, or persistence are the behavior under test; the generated starter verifies that todos survive a new application instance against the same temporary SQLite database. Avoid monkeypatching route dispatch or internal Tenchi functions—the purpose of these tests is to exercise the real boundary.
Verify store adapters
Run Tenchi's conformance checks against each idempotency or rate-limit adapter you plan to deploy:
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from tenchi.idempotency import IdempotencyStore
from tenchi.testing import verify_idempotency_store
class TestClock:
def __init__(self, value: float = 1_000.0) -> None:
self.value = value
def __call__(self) -> float:
return self.value
def advance(self, seconds: float) -> None:
self.value += seconds
clock = TestClock()
@asynccontextmanager
async def open_store() -> AsyncGenerator[IdempotencyStore]:
async with open_test_connection() as connection:
try:
yield PostgresIdempotencyStore(connection, clock=clock)
except BaseException:
await connection.rollback()
raise
else:
await connection.commit()
async def test_postgres_idempotency_store_conforms() -> None:
await verify_idempotency_store(open_store, advance=clock.advance)Replace open_test_connection() and PostgresIdempotencyStore with your
adapter's test connection and constructor. open_store() must open a fresh
independently usable scope over one shared test backend. A successful scope
must persist before it exits. The adapter and the advance(seconds) callback
must use the same deterministic clock; the callback may be synchronous or
asynchronous.
verify_idempotency_store() checks the reservation, replay, conflict,
expiration, token-fencing, identity-isolation, and concurrent-winner rules.
Use verify_rate_limit_store() with the same factory shape for fixed-window
capacity, rejected costs, reset boundaries, policy changes, isolation, and
concurrent admission. Failures raise StoreConformanceError with the store
kind, case name, and reason.
These checks verify the protocol state machine. Keep separate integration tests for behavior outside that contract: transaction rollback with application writes, migrations, cleanup, backend clock configuration, and coordination across the processes or hosts you deploy.
Keep OpenAPI reproducible
Generated applications include a test that runs:
from tenchi.cli import main
def test_openapi_snapshot_is_current() -> None:
assert main(["openapi", "--check", "openapi.json"]) == 0This checks exact drift. Compatibility belongs in CI against a historical baseline; see OpenAPI and compatibility.
Generated applications also keep the machine-facing tool contract current:
from tenchi.cli import main
def test_tool_snapshot_is_current() -> None:
assert main(["tools", "--check", "tools.json"]) == 0Run tenchi tools --diff against the previous snapshot before replacing it.
The exact test proves reproducibility; the historical diff proves
compatibility. See Application tools for the complete workflow.