How to set up session-level database fixtures for testing in a Python application. It runs migrations once per session to improve efficiency. It demonstrates how to truncate all tables between tests using SQL TRUNCATE ... CASCADE, and how to properly dispose of the database manager afterward — ensuring a clean, reliable, and performant async DB testing environment.How to set up session-level database fixtures for testing in a Python application. It runs migrations once per session to improve efficiency. It demonstrates how to truncate all tables between tests using SQL TRUNCATE ... CASCADE, and how to properly dispose of the database manager afterward — ensuring a clean, reliable, and performant async DB testing environment.

How to Set Up Session-Level Database Migrations in Python

2025/11/10 22:16
3분 읽기
이 콘텐츠에 대한 의견이나 우려 사항이 있으시면 crypto.news@mexc.com으로 연락주시기 바랍니다

Following my previous post about setting a function-level database setup, which is a junior-level solution, we’ll be looking at a session-level database migration setup.

When initiating a DB-coupled application, one of the initial goals is to set a DB connection function or class for spawning a reliable asynchronous connection with our DB.

Let us cover the most important parts of our setup.

The DB session manager class:

from sqlalchemy.ext.asyncio import ( async_sessionmaker, create_async_engine, AsyncEngine, AsyncSession, ) class DBSessionManager: def __init__(self, postgres_dsn: str): self._engine: AsyncEngine = create_async_engine(url=postgres_dsn) self._async_sesionmaker = async_sessionmaker( bind=self._engine, expire_on_commit=False ) @property def asessionmaker(self) -> async_sessionmaker[AsyncSession]: return self._async_sesionmaker async def close(self): await self._engine.dispose()

\ With the same set of models:

class Product(Base): __tablename__ = 'product' id: Mapped[UUID] = mapped_column( type_=types.UUID, primary_key=True, server_default=text('gen_random_uuid()'), ) name: Mapped[str] = mapped_column( type_=types.VARCHAR(100), server_default=text("''") ) created_at: Mapped[timestamp] = mapped_column( type_=types.TIMESTAMP, server_default=text('NOW()'), ) class Review(Base): __tablename__ = 'review' id: Mapped[UUID] = mapped_column( type_=types.UUID, primary_key=True, server_default=text('gen_random_uuid()'), ) content: Mapped[str] = mapped_column( type_=types.VARCHAR(1000), server_default=text("''") ) rating: Mapped[int] = mapped_column(type_=types.DECIMAL(2, 1)) created_at: Mapped[timestamp] = mapped_column( type_=types.TIMESTAMP, server_default=text('NOW()'), )

\

:::info Note: the test setup file is still the bottleneck of our test environment setup.

:::

\

The Essence of the Fixture Setup

The key fixtures to implement in a Python application with a database connection include:

  • Database creation and teardown
  • Schema version upgrades and downgrades
  • Connection setup and database content reset

\

@pytest_asyncio.fixture(scope='session') async def create_test_db(os_environ_patch): test_db_name = 'example_db_test' engine = create_async_engine( os.environ['POSTGRES_DSN_ORIGINAL'], isolation_level='AUTOCOMMIT', ) create_db_op = text(f'CREATE DATABASE {test_db_name}') drop_db_op = text(f'DROP DATABASE IF EXISTS {test_db_name} WITH (FORCE)') async with engine.begin() as conn: await conn.execute(create_db_op) yield async with engine.connect() as conn: await conn.execute(drop_db_op) @pytest.fixture(scope='session') def migrate_db(create_test_db): config = Config('alembic.ini') test_db_url = os.environ['POSTGRES_DSN'] config.set_main_option('sqlalchemy.url', test_db_url) command.upgrade(config, 'head') yield command.downgrade(config, 'base') @pytest_asyncio.fixture async def db(migrate_db) -> AsyncGenerator[DBSessionManager, None]: postgres_dsn = os.environ['POSTGRES_DSN'] db_manager = DBSessionManager(postgres_dsn) yield db_manager target_metadata = Base.metadata tables = target_metadata.tables.keys() all_tables_str = ', '.join(f'"{t}"' for t in tables) async with db_manager.asessionmaker() as s: await s.execute(text(f'TRUNCATE TABLE {all_tables_str} CASCADE')) await s.commit() await db_manager.close()

\ Now, let’s zoom in on the most important parts.

Migrations

@pytest.fixture(scope='session') def migrate_db(create_test_db):

The above lets us run through the migration step only once per session.

Tables truncation

Here, the DB fixture is relying on the session manager to execute custom SQL transactions.

\

target_metadata = Base.metadata tables = target_metadata.tables.keys() # dict_keys(['product', 'review']) all_tables_str = ', '.join(f'"{t}"' for t in tables) # '"product", "review"'

The code above extracts the registered tables to the comma-separated and quotation marks-wrapped representation.

After that, TRUNCATE TABLE {all_tables_str} CASCADE will delete all the records in the tables using cascade mode by deleting records in the constraints-dependent tables.

The final step is to dispose of the DB manager instance

await db_manager.close()

This way, we are ensured the migration process is set up correctly within our Python application.

\

면책 조항: 본 사이트에 재게시된 글들은 공개 플랫폼에서 가져온 것으로 정보 제공 목적으로만 제공됩니다. 이는 반드시 MEXC의 견해를 반영하는 것은 아닙니다. 모든 권리는 원저자에게 있습니다. 제3자의 권리를 침해하는 콘텐츠가 있다고 판단될 경우, crypto.news@mexc.com으로 연락하여 삭제 요청을 해주시기 바랍니다. MEXC는 콘텐츠의 정확성, 완전성 또는 시의적절성에 대해 어떠한 보증도 하지 않으며, 제공된 정보에 기반하여 취해진 어떠한 조치에 대해서도 책임을 지지 않습니다. 본 콘텐츠는 금융, 법률 또는 기타 전문적인 조언을 구성하지 않으며, MEXC의 추천이나 보증으로 간주되어서는 안 됩니다.

$30,000 in PRL + 15,000 USDT

$30,000 in PRL + 15,000 USDT$30,000 in PRL + 15,000 USDT

Deposit & trade PRL to boost your rewards!