From litestar-skills
Auto-activate for test_*.py, conftest.py in a Litestar project, litestar.testing imports (TestClient, AsyncTestClient, create_test_client). Litestar-specific testing: TestClient vs AsyncTestClient, @pytest.mark.anyio, app + lifespan, fixtures from canonical Litestar apps, mocking Guards / DI dependencies, integration with pytest-databases, and request-body / header / cookie / response assertions. Produces pytest fixtures, async tests, Guard mocks, DI overrides, AsyncTestClient setups, and pytest-databases-backed integration tests for Litestar apps. Use when: writing tests for a Litestar app, mocking auth/Guards, integration-testing handlers + DB, exercising HTMX or Inertia handlers. Not for generic non-Litestar pytest patterns (use the upstream pytest docs) or for Vitest/JS tests (one-liner pointer below).
npx claudepluginhub litestar-org/litestar-skills --plugin litestar-skillsThis skill uses the workspace's default tool permissions.
Litestar-specific testing patterns built on pytest + anyio. Covers:
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Builds scalable data pipelines, modern data warehouses, and real-time streaming architectures using Spark, dbt, Airflow, Kafka, and cloud platforms like Snowflake, BigQuery.
Builds production Apache Airflow DAGs with best practices for operators, sensors, testing, and deployment. For data pipelines, workflow orchestration, and batch job scheduling.
Litestar-specific testing patterns built on pytest + anyio. Covers:
TestClient vs AsyncTestClient — when to use each@pytest.mark.anyio setuplitestar-fullstack-spa testspytest-databases (see ../pytest-databases/SKILL.md)For JS-side testing (Vitest, Testing Library, Playwright), use the upstream Vitest docs and Litestar's own JS examples. Out of scope here.
T | None, never Optional[T]from __future__ import annotations — they are pure consumer code.@pytest.mark.anyio (not @pytest.mark.asyncio); Litestar uses anyio internallyAsyncTestClient for new code; TestClient only for legacy / sync-only flows| Client | When to Use | Lifespan | Internals |
|---|---|---|---|
TestClient | Sync test bodies, simple smoke tests | Triggered via context manager | Runs ASGI in a thread pool |
AsyncTestClient | Default for new tests — async test bodies, lifespan-aware fixtures | Native async lifespan | Runs ASGI in the test event loop |
# AsyncTestClient — preferred
from litestar.testing import AsyncTestClient
async def test_index(async_client: AsyncTestClient):
resp = await async_client.get("/")
assert resp.status_code == 200
# TestClient — legacy / sync
from litestar.testing import TestClient
def test_index(client: TestClient):
resp = client.get("/")
assert resp.status_code == 200
# conftest.py
import pytest
@pytest.fixture
def anyio_backend() -> str:
return "asyncio"
# tests/test_x.py
import pytest
@pytest.mark.anyio
async def test_something():
...
Litestar's runtime is anyio-based; do not use pytest-asyncio — it conflicts.
# conftest.py
from collections.abc import AsyncGenerator
import pytest
from litestar import Litestar
from litestar.testing import AsyncTestClient
from app import create_app
@pytest.fixture
async def app() -> Litestar:
return create_app()
@pytest.fixture
async def async_client(app: Litestar) -> AsyncGenerator[AsyncTestClient, None]:
async with AsyncTestClient(app=app) as client:
yield client
async with AsyncTestClient(...) runs on_startup / on_shutdown hooks and plugin lifespans (Vite, SAQ, SQLAlchemy session pool, etc.). Without the context manager, lifespan does not fire.
Guards are functions of (connection, route_handler) -> None. Mock by overriding dependencies or by registering a no-op guard at the app level for tests:
# conftest.py
from litestar import Litestar
from app import create_app
@pytest.fixture
async def app_with_no_auth() -> Litestar:
"""App with auth Guard replaced by a no-op for tests."""
from app.domain.accounts.guards import requires_active_user
async def allow_all(connection, route_handler) -> None:
return None
app = create_app()
# Swap the guard everywhere it's referenced (depends on app structure)
for route in app.route_handler_method_map.values():
...
return app
Cleaner: use DI override (preferred). If the Guard depends on a service via DI, override the service:
@pytest.fixture
async def async_client(app: Litestar) -> AsyncGenerator[AsyncTestClient, None]:
from app.domain.accounts.services import UserService
class FakeUserService(UserService): ...
app.dependencies["users_service"] = lambda: FakeUserService(...)
async with AsyncTestClient(app=app) as client:
yield client
from unittest.mock import AsyncMock
@pytest.fixture
async def async_client(app: Litestar) -> AsyncGenerator[AsyncTestClient, None]:
fake_email = AsyncMock()
app.dependencies["email_service"] = lambda: fake_email
async with AsyncTestClient(app=app) as client:
yield client, fake_email
Combine pytest-databases fixtures with the app fixture. See ../pytest-databases/SKILL.md.
# conftest.py
pytest_plugins = ["pytest_databases.docker.postgres"]
@pytest.fixture
async def app(postgres_service) -> Litestar:
from app import create_app
from app.config import Settings
settings = Settings(database_url=f"postgresql+asyncpg://{postgres_service.user}:{postgres_service.password}@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}")
return create_app(settings=settings)
The postgres_service fixture starts a Postgres container. Inject its connection details into the app config.
| Body Type | Pass via |
|---|---|
| JSON | client.post("/", json={...}) |
| Form | client.post("/", data={...}) |
| Multipart (file upload) | client.post("/", files={"file": ("name.txt", b"content", "text/plain")}) |
| Raw bytes | client.post("/", content=b"...") |
| Custom content-type | client.post("/", content=b"...", headers={"Content-Type": "..."}) |
async def test_create_user(async_client):
resp = await async_client.post(
"/api/users",
json={"name": "Alice", "email": "alice@example.com"},
)
assert resp.status_code == 201
body = resp.json()
assert body["name"] == "Alice"
# Header
resp = await async_client.get("/", headers={"Authorization": "Bearer token"})
# Cookie
async_client.cookies.set("session", "abc123")
resp = await async_client.get("/")
# Per-request cookies
resp = await async_client.get("/", cookies={"session": "abc123"})
async def test_htmx_partial(async_client):
resp = await async_client.get(
"/items/list",
headers={"HX-Request": "true", "HX-Target": "#item-list"},
)
assert resp.status_code == 200
assert "<ul" in resp.text
# Status
assert resp.status_code == 200
# Body
assert resp.json() == {"id": 1, "name": "Alice"}
# Headers
assert resp.headers["content-type"].startswith("application/json")
assert "HX-Trigger" in resp.headers
# Cookies (set by server)
assert "session" in resp.cookies
import pytest
@pytest.mark.parametrize("payload, expected_status", [
({"name": "valid", "email": "a@b.co"}, 201),
({"name": "", "email": "a@b.co"}, 400),
({"name": "valid", "email": "not-email"}, 400),
])
@pytest.mark.anyio
async def test_create_user_validation(async_client, payload, expected_status):
resp = await async_client.post("/api/users", json=payload)
assert resp.status_code == expected_status
pytest --cov=src --cov-report=html
pytest --cov=src --cov-fail-under=90
<workflow>
Add anyio_backend fixture to conftest.py returning "asyncio". Mark async tests with @pytest.mark.anyio.
Build an app fixture that returns a fresh Litestar instance per test (or per session if no shared state). Build an async_client fixture that wraps the app in AsyncTestClient via async with.
If the app talks to a DB, layer in pytest-databases (postgres_service, mysql_service, etc.) and pass connection details into the app config. See ../pytest-databases/SKILL.md.
Mock EmailService, HTTP clients, and other side-effect-laden dependencies via app.dependencies[name] = lambda: fake. Avoid real network calls in tests.
For tests that should bypass auth, override the Guard's underlying service or register a no-op Guard. Prefer DI overrides over patching internals.
@pytest.mark.parametrize for input variations.AsyncTestClient for new code.pytest --cov=src --cov-fail-under=90. Cover handlers, services, Guards, and at least one happy-path + one error-path per route.
@pytest.mark.anyio, not @pytest.mark.asyncio — Litestar runs on anyio. Mixing breaks lifespan.async with AsyncTestClient(app=app) — without the context manager, plugin lifespans (Vite, SAQ, SQLAlchemy) never run, and tests see a half-initialized app.AsyncTestClient over TestClient for new tests — the async client matches Litestar's runtime model.pytest-databases for real DB testing — never mock SQLAlchemy / sqlspec internals; assertions on mocked queries don't catch real bugs.HX-Request: true — handlers that branch on request.htmx need both branches covered.InMemoryConfig — see ../litestar-email/SKILL.md.Before delivering Litestar tests, verify:
anyio_backend fixture returns "asyncio"@pytest.mark.anyioAsyncTestClient is wrapped in async with (lifespan fires)pytest-databases fixturesHX-Request: true--cov-fail-under) is set in CITask: Test an account creation endpoint that hits Postgres, sends a welcome email via SAQ, and is guarded by an auth check.
# conftest.py
from collections.abc import AsyncGenerator
import pytest
from unittest.mock import AsyncMock
from litestar import Litestar
from litestar.testing import AsyncTestClient
pytest_plugins = ["pytest_databases.docker.postgres"]
@pytest.fixture
def anyio_backend() -> str:
return "asyncio"
@pytest.fixture
async def app(postgres_service) -> Litestar:
from app import create_app
from app.config import Settings
settings = Settings(
database_url=(
f"postgresql+asyncpg://{postgres_service.user}:{postgres_service.password}"
f"@{postgres_service.host}:{postgres_service.port}/{postgres_service.database}"
),
)
return create_app(settings=settings)
@pytest.fixture
async def async_client(app: Litestar) -> AsyncGenerator[tuple[AsyncTestClient, AsyncMock], None]:
fake_queue = AsyncMock()
app.dependencies["task_queues"] = lambda: type("Q", (), {"get": lambda self, name: fake_queue})()
async with AsyncTestClient(app=app) as client:
yield client, fake_queue
# tests/test_accounts.py
import pytest
@pytest.mark.anyio
async def test_create_account_persists_and_queues_email(async_client):
client, fake_queue = async_client
resp = await client.post(
"/api/accounts",
json={"email": "alice@example.com", "name": "Alice"},
)
assert resp.status_code == 201
body = resp.json()
assert body["email"] == "alice@example.com"
fake_queue.enqueue.assert_awaited_once()
args, kwargs = fake_queue.enqueue.await_args
assert args[0] == "send_welcome_email"
assert kwargs["email"] == "alice@example.com"
@pytest.mark.anyio
@pytest.mark.parametrize("payload, expected_status", [
({"email": "valid@example.com", "name": "Valid"}, 201),
({"email": "", "name": "Valid"}, 400),
({"email": "valid@example.com", "name": ""}, 400),
])
async def test_create_account_validation(async_client, payload, expected_status):
client, _ = async_client
resp = await client.post("/api/accounts", json=payload)
assert resp.status_code == expected_status
</example>
InMemoryConfig for email tests.For Vitest, Testing Library (React/Vue), and component testing, refer to upstream Vitest docs (https://vitest.dev/). This skill covers the Python/Litestar side only.