Skip to content
Menu

Documentation

sandloom.sdk.testing.plugin

pytest plugin of the 1.0 testing kit: the harness plus published and Contract resolution.

Activate it once per application (sandloom new does):

[tool.pytest.ini_options]
addopts = ["-p", "sandloom.sdk.testing.plugin"]

It is the framework harness (every fixture: client, db, real_uow_client, ...; the test environment; the isolation guards) plus, for every test:

  • published(E) records the events the code under test publishes;
  • Contract(REF) resolves through the test application's contracts whenever the test uses the application (any fixture built on sandloom_app); fake_contract overrides one for a block;
  • with SANDLOOM_TEST_OBSERVE_EFFECTS=1 (sandloom test outside --ci), the money / security effects each route ran are recorded for sandloom routes snapshot --write (informational observed_effects).

Activate this plugin or sandloom.core.testing.plugin, not both.

from sandloom.sdk.testing.plugin import <name>. Signatures in a terminal: sandloom docs <name>. Beginner names are the ones generated code and the examples use; any other name is advanced (mark its import line # sandloom: advanced <reason> in generated code and examples).

Start with (beginner)

Name Kind Summary
client fixture HTTP client on the app; every request runs on the test's db.

Advanced

Name Kind Summary
ambient_uow fixture Bind an in-memory recording UoW for the whole test.
async_client fixture
db fixture A session on the test database, rolled back after the test.
db_engine fixture Engine on the test database; every table present and empty.
db_uow fixture db bound as the UoW for the whole test (outbox / audit rows join it).
make_headers fixture Factory: make_headers(user_id=None, tenant_id=None, **claims).
real_uow_client fixture HTTP client whose requests run through the REAL request UoW.
revoked_tokens fixture Record revoke_user_tokens(user_id) calls instead of running them.
saga_harness fixture Drive the REAL saga protocol synchronously (sandloom.core.testing.sagas).
sandloom_app fixture The application's ASGI app, built by its composition root.
sandloom_cache fixture The app's default cache on the test CACHE_URL, emptied first.
sandloom_core fixture The application's CoreFacade.
sandloom_test_env fixture Load the nearest .env.test (rootdir first, then the test paths).
scratch_database fixture A throw-away database on the test server (sync URL), dropped after.
test_db fixture
test_engine fixture
user_headers fixture Headers of a default user (random user id and tenant).

Beginner names

client

@pytest.fixture
def client(
    sandloom_app: Any,
    db: Any,
    request: pytest.FixtureRequest,
) -> AsyncIterator[Any]

HTTP client on the app; every request runs on the test's db.

Shared-session semantics: every request uses the test's ONE db session (bound as the UoW), through DbSession and through db() alike (sandloom.core.testing._uow.SharedRequestUnit), so nothing commits and a request that fails with a 4xx does NOT roll its writes back: the rows a failed request flushed stay visible to the test (and to later requests) until the test ends. Assert rollback behaviour with real_uow_client, whose requests commit or roll back exactly as in production (both paths).

An endpoint that raises an unhandled exception re-raises it here (sandloom.core.testing.server_errors.UnhandledServerError: type, message, fix) instead of returning a bare 500 internal_error; mark a test @pytest.mark.sandloom_allow_server_errors to get the 500 response.

Advanced names

ambient_uow

@pytest.fixture
def ambient_uow() -> AsyncIterator[Any]

Bind an in-memory recording UoW for the whole test.

async_client

@pytest.fixture
def async_client(client: Any) -> Any

(no docstring)

db

@pytest.fixture
def db(db_engine: Any) -> AsyncIterator[Any]

A session on the test database, rolled back after the test.

db_engine

@pytest.fixture
def db_engine(sandloom_app: Any) -> AsyncIterator[Any]

Engine on the test database; every table present and empty.

db_uow

@pytest.fixture
def db_uow(db: Any) -> AsyncIterator[Any]

db bound as the UoW for the whole test (outbox / audit rows join it).

make_headers

@pytest.fixture
def make_headers(
    sandloom_test_env: Path,
) -> Callable[..., tuple[Any, dict[str, str]]]

Factory: make_headers(user_id=None, tenant_id=None, **claims).

real_uow_client

@pytest.fixture
def real_uow_client(
    sandloom_app: Any,
    sandloom_core: Any,
    db_engine: Any,
    request: pytest.FixtureRequest,
) -> AsyncIterator[Any]

HTTP client whose requests run through the REAL request UoW.

No get_db override: the app's default database (and cache) are pointed at the test DATABASE_URL / CACHE_URL, so every request commits or rolls back exactly as in production — a 4xx rolls its writes back (committed rows are emptied by the next test's db_engine). An unhandled endpoint error re-raises like with client (@pytest.mark.sandloom_allow_server_errors keeps the 500 response).

revoked_tokens

@pytest.fixture
def revoked_tokens(monkeypatch: pytest.MonkeyPatch) -> RevokedTokens

Record revoke_user_tokens(user_id) calls instead of running them.

Keeps API tests deterministic (a token minted in the same second as a revocation would otherwise flip to 401) and lets the test assert who was revoked; enforcement itself is covered by the core auth tests. A RevokedTokens list: .available = False simulates an outage.

saga_harness

@pytest.fixture
def saga_harness(
    sandloom_core: Any,
    db_engine: Any,
) -> AsyncIterator[Any]

Drive the REAL saga protocol synchronously (sandloom.core.testing.sagas).

The app's default database points at the test database; every transition commits for real (rows are emptied by the next test's db_engine). run() / fail_step / crash_after / deliver_twice / clock.advance / fake_contract.

sandloom_app

@pytest.fixture
def sandloom_app(
    pytestconfig: pytest.Config,
    sandloom_test_env: Path,
) -> Any

The application's ASGI app, built by its composition root.

sandloom_cache

@pytest.fixture
def sandloom_cache(sandloom_core: Any) -> AsyncIterator[Any]

The app's default cache on the test CACHE_URL, emptied first.

Yields the CacheProvider the app itself resolves (core.cache(), idempotency, rate limits, token revocation). The Redis database is FLUSHDB'd before the test so keys never leak between tests — so CACHE_URL must name a disposable test database. Also needed where the app was imported before .env.test was loaded (its settings snapshot names another cache), as in the framework's own suite.

sandloom_core

@pytest.fixture
def sandloom_core(sandloom_app: Any) -> Any

The application's CoreFacade.

sandloom_test_env

@pytest.fixture
def sandloom_test_env(request: pytest.FixtureRequest) -> Path

Load the nearest .env.test (rootdir first, then the test paths).

scratch_database

@pytest.fixture
def scratch_database(sandloom_test_env: Path) -> Iterator[str]

A throw-away database on the test server (sync URL), dropped after.

For a module's migration tests (sandloom.core.testing.migrations): walk its chain up and down without touching the shared test database.

test_db

@pytest.fixture
def test_db(db: Any) -> Any

(no docstring)

test_engine

@pytest.fixture
def test_engine(db_engine: Any) -> Any

(no docstring)

user_headers

@pytest.fixture
def user_headers(
    make_headers: Callable[..., tuple[Any, dict[str, str]]],
) -> dict[str, str]

Headers of a default user (random user id and tenant).