Skip to content

Pytest Plugin

Auto-registered via the pytest11 entry point — installed alongside Protean. No conftest.py registration required.

For task-oriented workflow guidance see Dual-Mode Testing for switching environments via --protean-env, and Fixtures and Patterns for DomainFixture and conftest.py recipes.

CLI options

Option Default Effect
--protean-env=<name> test Sets PROTEAN_ENV before test collection, so the matching [<name>] overlay in domain.toml applies when the domain is constructed at import time.
--update-snapshots off Causes assert_snapshot() calls to regenerate their reference files instead of comparing.

Registered markers

The plugin registers these markers so --strict-markers accepts them without configuration:

Marker Intent
domain Pure domain logic tests — no database
application Command/event handler tests — uses database
integration Cross-aggregate tests with real adapters
slow Tests taking more than a second or two
bdd Behavior-driven scenarios

Markers are tags for filtering with -m; they do not change test behavior on their own.

Protean's own test_domain fixture

The session-scoped autouse test_domain fixture described in some Protean test suites is defined in protean.integrations.pytest.adapter_conformance and is not auto-loaded by this plugin. It activates only when a conftest.py opts in explicitly:

pytest_plugins = ["protean.integrations.pytest.adapter_conformance"]

When opted in, tests that construct their own Domain(name="Test") should add @pytest.mark.no_test_domain so the session fixture is skipped for that test.

User application tests should construct their domain lifecycle with DomainFixture instead — see Fixtures and Patterns.

Query-shape assertions

Four context managers observe the SQL a block issues against a SQLAlchemy backend, to catch query-cost regressions in tests. Three assert query count and shape; capture_queries yields the raw statements for custom assertions such as insert ordering. They are no-ops when no SQLAlchemy engine is resolved (e.g. the in-memory adapter). See Test Query Shape for usage.

assert_query_count

assert_query_count(
    expected: int, engine: Optional[Engine] = None
) -> Iterator[List[str]]

Assert exactly expected queries are issued in the block.

A "query" is a data round trip (SELECT/INSERT/UPDATE/DELETE/ WITH); connection setup (PRAGMA/SET) and transaction control are not counted, so adapters that issue per-connection pragmas don't inflate the count. Catches extra round trips: a count_by_status per-status loop, a 1+N find-and-claim, etc. Yields the captured statements unfiltered (a test can inspect pragmas and transaction control too); the failure message lists only the counted queries. No-op when no SQLAlchemy engine is resolved.

assert_no_subquery_wrap

assert_no_subquery_wrap(
    engine: Optional[Engine] = None,
) -> Iterator[List[str]]

Fail if any query wraps a count around a subquery.

Catches the SELECT count(*) FROM (SELECT ... ) AS anon_1 shape that a naive .limit(1).all().total emits instead of a flat SELECT count(*). No-op when no SQLAlchemy engine is resolved.

assert_no_overfetch

assert_no_overfetch(
    expected_returned: int,
    ratio: float = 1.5,
    engine: Optional[Engine] = None,
) -> Iterator[List[Tuple[str, Any]]]

Fail if any LIMIT exceeds expected_returned * ratio.

Catches over-fetch patterns such as min(limit * 3, 1000) that pull far more rows than the caller needs. ratio is the tolerated headroom over the expected row count. Resolves LIMIT values whether rendered as literals or bound parameters. No-op when no SQLAlchemy engine is resolved.

capture_queries

capture_queries(
    engine: Optional[Engine] = None,
) -> Iterator[List[Tuple[str, Any]]]

Capture (statement, parameters) for each query in the block.

Unlike the assert_* helpers, this makes no assertion of its own; it just yields the captured statements so a test can make a custom assertion — e.g. that a parent table's INSERT is emitted before a child table's. The list is empty and nothing is observed when no SQLAlchemy engine is resolved (the in-memory adapter or no domain bound), so callers must guard on it and mark the test @pytest.mark.database (or a backend marker) to run it where the assertion is meaningful.

Framework development

The protean test CLI command is used to run Protean's own test suite across adapter configurations. It is not intended for user applications. See Testing Protean for details.