Testing with pytest¶
Installing sqlalchemyseed alongside pytest registers a plugin that loads
fixture files into a transactionally-isolated session. You provide a single
engine fixture; the plugin supplies sqlalchemyseed_session (rolled back
after every test) and a seed factory.
The plugin is registered automatically through a pytest11 entry point — no
configuration is required. pytest is only needed for testing; it is not a
runtime dependency of the library.
Setup¶
Define one engine fixture in your conftest.py that returns a SQLAlchemy
Engine with your schema created.
# conftest.py
import pytest
from sqlalchemy import create_engine, event
from sqlalchemy.pool import StaticPool
from myapp.models import Base
@pytest.fixture(scope="session")
def engine():
# StaticPool keeps a single in-memory connection alive so the schema
# you create is visible to the test session. A file-based or server
# database needs no such tweak -- just return your usual engine.
engine = create_engine(
"sqlite://",
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
# SQLite only: hand transaction control to SQLAlchemy so an explicit
# commit() inside a test lands on a savepoint and is rolled back with
# the outer transaction. Left to itself the pysqlite driver commits
# straight to the database and the per-test rollback cannot undo it.
# Other databases (PostgreSQL, MySQL) need neither listener.
@event.listens_for(engine, "connect")
def _sqlite_no_driver_begin(dbapi_connection, connection_record):
dbapi_connection.isolation_level = None
@event.listens_for(engine, "begin")
def _sqlite_emit_begin(connection):
connection.exec_driver_sql("BEGIN")
Base.metadata.create_all(engine)
return engine
Writing a test¶
Request the seed factory and seed a data file. Every test runs inside a
transaction that is rolled back afterward, so tests never see each other’s rows.
# test_people.py
from myapp.models import Person
def test_people_are_seeded(seed, sqlalchemyseed_session):
seeder = seed("tests/data/people.yaml")
assert sqlalchemyseed_session.query(Person).count() == 2
assert seeder.instances[0].name == "Alice"
Fixtures¶
The plugin adds three fixtures.
engineYou provide this in your
conftest.py. It is the single integration point: anEnginewith the schema already created. If you forget to define it, the plugin raises an error explaining exactly what to add.sqlalchemyseed_sessionA
Sessionbound to an open transaction that is rolled back after each test. Request it whenever a test needs to query the database. You can override it in your ownconftest.pyif you manage sessions yourself.seedA callable that loads a data file and seeds it into
sqlalchemyseed_session, returning the seeder so.instancesis available:seed(path, *, model=None, seeder="basic", ref_prefix="!")
path— a.json,.yaml/.yml, or.csvfile.model— required for CSV inputs, which are not self-describing, for exampleseed("people.csv", model="myapp.models.Person").seeder—"hybrid"to useHybridSeederinstead of the defaultSeeder.ref_prefix— override the relationship reference prefix (default!).
How isolation works¶
The sqlalchemyseed_session fixture opens a connection-level transaction and
binds a session to it with join_transaction_mode="create_savepoint". Any
commit issued during the test lands on a savepoint inside that transaction, and
the transaction is rolled back when the test finishes. Nothing is written to the
database permanently, and there is no per-test schema teardown, so the tests
stay fast and leave the database pristine.
For an explicit commit() to be caught by the savepoint, SQLAlchemy must
control the transaction boundaries. On SQLite the pysqlite driver manages
BEGIN itself by default, so the connect/begin event listeners in the
engine fixture above are required — without them a committed row escapes the
rollback. Seeded data (which is flushed, never committed) rolls back regardless.
Other databases such as PostgreSQL and MySQL need no such listeners.
Note
The plugin registers fixtures named engine, sqlalchemyseed_session,
and seed. Defining your own engine fixture is how you plug in your
database; if you already use those names for something else, your
definitions take precedence (pytest resolves conftest fixtures over
plugin fixtures).