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.