Skip to content

feat(testing): build_test_client(platform) — async context manager wrapping lifespan + httpx client #549

@bokelley

Description

@bokelley

Summary

build_asgi_app(platform) (PR #535) gives adopters the ASGI app they need for in-process integration tests. But every test that uses it writes the same 4 lines of boilerplate to wire the lifespan + transport + httpx client:

```python
from asgi_lifespan import LifespanManager
import httpx
from adcp.testing import build_asgi_app

async def test_something():
app = build_asgi_app(platform)
async with LifespanManager(app):
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url="http://test\",
) as client:
resp = await client.post("/mcp", json=...)
# finally — the actual test
```

A build_test_client(platform) helper that bundles those three layers into one async context manager collapses adopter test files significantly:

```python
from adcp.testing import build_test_client

async def test_something():
async with build_test_client(platform) as client:
resp = await client.post("/mcp", json=...)
```

Proposed shape

```python
@asynccontextmanager
async def build_test_client(
platform: DecisioningPlatform,
*,
base_url: str = "http://test\",
name: str | None = None,
advertise_all: bool = False,
auto_emit_completion_webhooks: bool = False,
headers: Mapping[str, str] | None = None,
**factory_kwargs: Any,
) -> AsyncIterator[httpx.AsyncClient]:
"""...
Yields an httpx.AsyncClient wired against the platform's ASGI app
via httpx.ASGITransport + LifespanManager. Closes both on exit.
"""
app = build_asgi_app(
platform,
name=name,
advertise_all=advertise_all,
auto_emit_completion_webhooks=auto_emit_completion_webhooks,
**factory_kwargs,
)
async with LifespanManager(app):
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url=base_url,
headers=dict(headers) if headers else None,
) as client:
yield client
```

~25 LOC of implementation; surface mirrors build_asgi_app so adopters who outgrow the convenience helper drop down to the lower layer with no API rewrite.

The headers= kwarg is the small extra value-add — adopters testing the auth path use headers={\"x-adcp-auth\": \"tok_...\"} once at the client level instead of per-request.

Migration impact

asgi_lifespan and httpx are already test-only deps in adopters using build_asgi_app. No new install. The helper makes them transitive for anyone using build_test_client (move them from per-test to a single import in their conftest).

Test count this would simplify

salesagent's core/tests/test_e2e_get_products.py and the M2-roadmap storyboard tests collapse by ~half. Multiplying across the framework's own test suite, the savings compound.

Related

Happy to PR this one if it'd help — small enough to be a 1-PR drive-by.

🤖 Filed via Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions