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.

engine

You provide this in your conftest.py. It is the single integration point: an Engine with the schema already created. If you forget to define it, the plugin raises an error explaining exactly what to add.

sqlalchemyseed_session

A Session bound 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 own conftest.py if you manage sessions yourself.

seed

A callable that loads a data file and seeds it into sqlalchemyseed_session, returning the seeder so .instances is available:

seed(path, *, model=None, seeder="basic", ref_prefix="!")
  • path — a .json, .yaml/.yml, or .csv file.

  • model — required for CSV inputs, which are not self-describing, for example seed("people.csv", model="myapp.models.Person").

  • seeder"hybrid" to use HybridSeeder instead of the default Seeder.

  • 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).