pytest Explained
pytest is new to this repo as of the Strategy Tracker tables. There was no automated test suite before it — backend/scratch/test.py is a one-off script, not a test file pytest would collect. This doc explains what pytest is, and — more specific to this repo — why these particular tests run against the real local Postgres database instead of an in-memory one, and how they avoid leaving junk rows behind when they do.
The one idea that matters
pytest finds every function named test_* in every file named test_*.py, runs each one, and reports which passed and which failed. A test "fails" when it raises an exception — usually from an assert statement, or, in this repo's case, from pytest.raises(...) not catching the exception it expected.
cd backend
python -m pytest tests/test_strategy_models.py -v
-v (verbose) prints one line per test with its name and PASSED/FAILED, instead of just a summary count.
Why these tests hit real Postgres, not SQLite or a mock
The requirement being tested is specific: "inserting a strategy or regime_master row without hypothesis raises IntegrityError, enforced at the schema level." That's a claim about what Postgres itself does when a NOT NULL column is violated — not about what the Python model class does. A mock, or an in-memory SQLite database, would only prove the test author's assumptions about Postgres's behavior, not Postgres's actual behavior. So tests/conftest.py connects to the same local tedb the app uses (settings.SYNC_DATABASE_URL, LOCAL_DB_* from backend/.env) and lets the real NOT NULL constraint fire for real.
The problem this creates, and the fix: SAVEPOINT per test
Testing against a shared, real database creates an obvious risk: every test run leaves rows behind, and a failing INSERT (which is exactly what these tests deliberately cause) can leave the whole database connection's transaction in an unusable state for whatever runs next. tests/conftest.py solves both with one fixture:
@pytest.fixture
def db_session(engine):
connection = engine.connect()
trans = connection.begin()
session_factory = sessionmaker(bind=connection, join_transaction_mode="create_savepoint")
session = session_factory()
yield session
session.close()
trans.rollback()
connection.close()
Two things are doing the work here:
- The outer
trans = connection.begin()/trans.rollback()— every test's fixture opens one real transaction and always rolls it back at the end, pass or fail. Nothing a test does can outlive the test. join_transaction_mode="create_savepoint"(a SQLAlchemy 2.0 option) — this is the part that matters when a test expects anIntegrityError. Without it, a failedINSERTwould abort the entire outer transaction at the database level, and anysession.rollback()call inside the test would have nothing left to roll back to except the very start — fragile, and it silently breaks if a test ever needs to do anything after the expected failure. With it, SQLAlchemy wraps each test's work in aSAVEPOINTinstead of the outer transaction directly, sosession.rollback()inside the test cleanly rewinds to that savepoint, and the fixture's owntrans.rollback()still guarantees a clean slate afterward regardless.
The test itself is then just: build the row, omit hypothesis, and assert Postgres complains.
def test_strategy_without_hypothesis_raises_integrity_error(db_session):
strat = Strategy(
strategy_code=_unique("strat"),
param_hash=_unique("hash"),
canonical_json="{}",
display_name="Test Strategy",
symbol="NIFTY",
timeframe="5min",
# hypothesis intentionally omitted -> NOT NULL violation expected
)
db_session.add(strat)
with pytest.raises(IntegrityError):
db_session.flush()
db_session.rollback()
_unique(...) (a small helper using uuid4) exists only so strategy_code/param_hash — both UNIQUE columns — never collide with a leftover row from a previous run or a parallel test session; it has nothing to do with the hypothesis check itself.
Where this generalizes
Any future test that needs to prove something about a real Postgres constraint (a CHECK, a UNIQUE, a foreign key) rather than app-level validation logic can reuse the same db_session fixture pattern from tests/conftest.py — it isn't specific to the Strategy Tracker tables, just first introduced alongside them.