From litestar
Provides Litestar-specific testing patterns with TestClient, AsyncTestClient, anyio setup, Guard mocking, and DI overrides for pytest-based test suites.
How this skill is triggered — by the user, by Claude, or both
Slash command
/litestar:litestar-testingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Litestar-specific testing patterns built on pytest + anyio. Covers:
Litestar-specific testing patterns built on pytest + anyio. Covers:
TestClient vs AsyncTestClient — when to use each@pytest.mark.anyio setuppytest-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 by default; do not mix AnyIO and pytest-asyncio auto modes.AsyncTestClient 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": "[email protected]"},
)
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": "[email protected]"}, 201),
({"name": "", "email": "[email protected]"}, 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
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 for new Litestar async tests — keep pytest-asyncio only when a project already uses it explicitly, and never mix auto modes.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.backend="memory" / InMemoryBackend — 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": "[email protected]", "name": "Alice"},
)
assert resp.status_code == 201
body = resp.json()
assert body["email"] == "[email protected]"
fake_queue.enqueue.assert_awaited_once()
args, kwargs = fake_queue.enqueue.await_args
assert args[0] == "send_welcome_email"
assert kwargs["email"] == "[email protected]"
@pytest.mark.anyio
@pytest.mark.parametrize("payload, expected_status", [
({"email": "[email protected]", "name": "Valid"}, 201),
({"email": "", "name": "Valid"}, 400),
({"email": "[email protected]", "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
backend="memory" and InMemoryBackend 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.
npx claudepluginhub litestar-org/litestar-skills --plugin litestarSearches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.