agentsclimarketplace

Litestar testing

Skill litestar-org/litestar-skills/skills/litestar-testing

Opinionated first-party agent skills, plugins, subagents, slash commands, and MCP servers for the Litestar framework ecosystem — publishable to Claude Code, Gemini CLI, Codex CLI, Cursor, OpenCode, and VS Code/Copilot from a single repo.

Install
npx -y skills add litestar-org/litestar-skills --skill litestar-testing

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Auto-activate for test_*.py, conftest.py, litestar.testing, TestClient, AsyncTestClient, create_test_client, create_async_test_client, anyio, Guard mocks, DI overrides, or handler tests. Not for generic pytest.

SKILL.md

14.7 KB, as published. Nobody here has run it

litestar-testing

Litestar-specific testing patterns built on pytest + anyio. Covers:

  • TestClient vs AsyncTestClient — when to use each
  • @pytest.mark.anyio setup
  • App + lifespan in tests
  • Fixture patterns from canonical litestar-fullstack tests
  • Mocking Guards and DI dependencies
  • Integration with pytest-databases (see ../pytest-databases/SKILL.md)
  • Autowire discovery and cache isolation (see ../litestar-autowire/references/testing.md)
  • Request body / form / multipart / header / cookie testing
  • Litestar-specific assertion patterns (Response, headers, cookies)

For JS-side testing (Vitest, Testing Library, Playwright), use the upstream Vitest docs and Litestar's own JS examples. Out of scope here.

Code Style Rules

  • PEP 604 unions: T | None, never Optional[T]
  • Test modules MAY use from __future__ import annotations — they are pure consumer code.
  • Function-based tests (not class-based)
  • One assertion concern per test
  • Async Litestar tests use @pytest.mark.anyio by default; do not mix AnyIO and pytest-asyncio auto modes.
  • Prefer AsyncTestClient for new code; TestClient only for legacy / sync-only flows

Quick Reference

TestClient vs AsyncTestClient

ClientWhen to UseLifespanInternals
TestClientSync test bodies, simple smoke testsTriggered via context managerRuns ASGI in a thread pool
AsyncTestClientDefault for new tests — async test bodies, lifespan-aware fixturesNative async lifespanRuns 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

anyio Setup

# 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.

App + Lifespan Fixture

# 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.

Mocking Guards

Guards are functions of (connection, route_handler) -> None. Test the real guard with fake identity or authorization providers. Build a fresh app with replacement providers; Litestar has no mutable app.dependency_overrides registry.

from litestar.di import Provide


@pytest.fixture
async def async_client() -> AsyncGenerator[AsyncTestClient, None]:
    fake_users_service = FakeUserService()

    async def provide_fake_users_service() -> UserService:
        return fake_users_service

    test_app = create_app(
        dependencies={
            "users_service": Provide(provide_fake_users_service),
        },
    )
    async with AsyncTestClient(app=test_app) as client:
        yield client

Mocking DI Dependencies

from collections.abc import AsyncGenerator
from unittest.mock import AsyncMock

import pytest
from litestar.di import Provide
from litestar.testing import AsyncTestClient


@pytest.fixture
async def async_client() -> AsyncGenerator[tuple[AsyncTestClient, AsyncMock], None]:
    fake_email = AsyncMock()

    async def provide_fake_email() -> AsyncMock:
        return fake_email

    app = create_app(
        dependencies={
            "email_service": Provide(provide_fake_email),
        },
    )
    async with AsyncTestClient(app=app) as client:
        yield client, fake_email

For isolated handler tests, pass replacements directly to create_async_test_client(..., dependencies={...}). Do not mutate a constructed app; rebuilding preserves dependency resolution and prevents parallel tests from sharing overrides.

Integration with pytest-databases

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.

Request Bodies

Body TypePass via
JSONclient.post("/", json={...})
Formclient.post("/", data={...})
Multipart (file upload)client.post("/", files={"file": ("name.txt", b"content", "text/plain")})
Raw bytesclient.post("/", content=b"...")
Custom content-typeclient.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"

Headers, Cookies, Auth

# 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"})

HTMX Requests

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

Response Assertions

# 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

Parametrize

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

Coverage

pytest --cov=src --cov-report=html
pytest --cov=src --cov-fail-under=90
<workflow>

Workflow

Step 1: Set Up anyio Backend

Add anyio_backend fixture to conftest.py returning "asyncio". Mark async tests with @pytest.mark.anyio.

Step 2: App + Client Fixtures

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.

Step 3: Add Database Fixtures

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.

Step 4: Override DI for Externals

Mock EmailService, HTTP clients, and other side-effect-laden dependencies by constructing a fresh app or test client with replacement Provide instances. Avoid real network calls in tests.

Step 5: Mock Guards When Needed

Build a fresh app with fake identity or authorization providers. Register a no-op guard only when the test intentionally excludes authentication behavior; never patch route internals on a shared app.

Step 6: Write Tests

  • One assertion concern per test.
  • Use @pytest.mark.parametrize for input variations.
  • Use AsyncTestClient for new code.
  • Include HTMX / Inertia headers when testing those paths.

Step 7: Verify Coverage

pytest --cov=src --cov-fail-under=90. Cover handlers, services, Guards, and at least one happy-path + one error-path per route.

</workflow> <guardrails>

Guardrails

  • Use @pytest.mark.anyio for new Litestar async tests — keep pytest-asyncio only when a project already uses it explicitly, and never mix auto modes.
  • Always async with AsyncTestClient(app=app) — without the context manager, plugin lifespans (Vite, SAQ, SQLAlchemy) never run, and tests see a half-initialized app.
  • Prefer AsyncTestClient over TestClient for new tests — the async client matches Litestar's runtime model.
  • Mock side effects via DI override, not patching — keeps tests isolated from import order and global state.
  • Build a fresh app for dependency replacements — Litestar has no mutable dependency-override registry, and shared app mutation races under parallel tests.
  • Use pytest-databases for real DB testing — never mock SQLAlchemy / sqlspec internals; assertions on mocked queries don't catch real bugs.
  • Function-based tests — no class-based test containers unless absolutely needed for shared setup.
  • One assertion concern per test — failures should pinpoint a single behavior.
  • Don't share state between tests — fresh app + fresh DB per test (or per module with explicit cleanup).
  • Test the HTMX path with HX-Request: true — handlers that branch on request.htmx need both branches covered.
  • Mock email via backend="memory" / InMemoryBackend — see ../litestar-email/SKILL.md.
</guardrails> <validation>

Validation Checkpoint

Before delivering Litestar tests, verify:

  • anyio_backend fixture returns "asyncio"
  • Async tests use @pytest.mark.anyio
  • AsyncTestClient is wrapped in async with (lifespan fires)
  • DI dependencies (email, HTTP clients) are overridden, not patched
  • DB-dependent tests use pytest-databases fixtures
  • Guards either pass real auth or use a fresh app with fake identity providers
  • One assertion concern per test; parametrize for input variations
  • HTMX-targeted handlers have tests with HX-Request: true
  • Coverage gate (--cov-fail-under) is set in CI
</validation> <example>

Example

Task: 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
from unittest.mock import AsyncMock, Mock

import pytest
from litestar import Litestar
from litestar.di import Provide
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) -> tuple[Litestar, AsyncMock]:
    from app import create_app
    from app.config import Settings

    fake_queue = AsyncMock()
    fake_task_queues = Mock()
    fake_task_queues.get.return_value = fake_queue

    async def provide_fake_task_queues() -> Mock:
        return fake_task_queues

    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,
            dependencies={
                "task_queues": Provide(provide_fake_task_queues),
            },
        ),
        fake_queue,
    )


@pytest.fixture
async def async_client(
    app: tuple[Litestar, AsyncMock],
) -> AsyncGenerator[tuple[AsyncTestClient, AsyncMock], None]:
    test_app, fake_queue = app
    async with AsyncTestClient(app=test_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
</example>

References Index

  • Async Testing — anyio setup, async fixtures, context manager testing, and common pitfalls.

Cross-References

JS-side Testing

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.

Official References

Shared Styleguide Baseline

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.