Stateful Testing
Some bugs only surface after a sequence of writes — pagination boundaries
that drift after deletes, RLS policies that allow unintended access only
after a role change, aggregate functions that lose precision under
incremental updates. Hypothesis’s RuleBasedStateMachine is the right tool
for these, and SqlProofStateMachine wires it into SqlProof so each example
gets its own isolated Postgres state.
Quick example
Section titled “Quick example”Verify that two RLS helpers (get_member_project_ids, get_editor_project_ids)
correctly track the contents of project_members, no matter how membership
churns:
API reference
Section titled “API reference”SqlProofStateMachine
Section titled “SqlProofStateMachine”Subclass and define rules and invariants. Important attributes:
self.db: SqlProofClient— the live client for the current example. Inside a savepoint that’s rolled back when the example ends.initial_dataset: ClassVar[dict] = {}— class attribute. If you set a non-empty dict, every example starts with that data already inserted.
Override on_setup(self) -> None to seed example-specific fixtures (auth
users, projects, JWT claims). Don’t override __init__ or teardown —
SqlProof manages those.
self.enter(cm)
Section titled “self.enter(cm)”Adopts a context manager for the duration of the current example. Closes in reverse-entry order during teardown. Use it for resources that must live across rules but not across examples:
SqlProof.run_state_machine(machine_class, *, settings=None)
Section titled “SqlProof.run_state_machine(machine_class, *, settings=None)”Binds the proof to a transient subclass of your machine and dispatches to
hypothesis.stateful.run_state_machine_as_test. The transient subclass is
internal — your class definition stays clean and runnable from any
fixture-equipped test:
Direct instantiation (MembershipMachine()) without run_state_machine
raises SqlProofUsageError — this is intentional, so the failure mode is
self-explanatory.
When to reach for a state machine
Section titled “When to reach for a state machine”Use a state machine when:
- The function under test is a fold over accumulated state (windowed aggregates, running totals, paginated views).
- The bug class is “works the first time, fails after N operations” — off-by-ones in pagination, stale caches, RLS policies that don’t re-evaluate.
- You can describe the correct behavior as an in-Python model that mirrors the DB state.
Stick with single-shot property tests (@given(...)) when the function
under test is purely a function of its input — extract_domain(url),
is_competitor_mentioned(name, comps). State machines have higher
overhead and slower shrinking; don’t reach for them when a one-shot
example would catch the bug.
See also
Section titled “See also”- SqlProof class API —
client_for_dataset,dataset_strategy, and the rest of the proof surface. - Supabase contrib —
as_supabase_userfor RLS testing in tandem with state machines.