Testing#

Similar to the actual library source itself, the tests are subdivided primarily by “feature” (i.e. schema/view/grant/etc).

There are a couple of key organizational factors that should become apparent, looking at the tests’ folder structure:

tests/
    examples/
        test_view_create_pg/
        test_view_create_mysql/
        ...
    view/
        test_alembic.py
        test_materialized.py
        ...
    ...

Alembic tests#

Generally, alembic tests are much slower and more difficult to debug than regular tests. Therefore they should be limited as much as possible to the things required to get proper code-coverage of a feature within alembic.

When testing variants of a feature where the only difference would be equally well tested when running a metadata.create_all() call, you’re better off avoiding alembic-based tests and using the “SQLAlchemy tests” examples below.

Now, to the details:

First, examples/. Due to the way alembic works internally, in order to test the alembic integration, we need to have a whole “fake” alembic migrations history in a standalone folder, so that we can see whether a given feature works end-to-end once running inside alembic.

Therefore, each folder inside the examples/ folder is a standalone pseudo “project” with models, migrations, and tests. The tests for each pseudo-project invoke pytest using pytest-alembic, in order to run the alembic migrations against a real database.

These examples are invoked using Pytest’s pytester fixture, which automates the calling of the examples’ pytest inline or in a subprocess.

All of the alembic/examples-based tests for a given feature live in tests/<feature>/test_alembic.py:

import pytest

from tests.utilities import successful_test_run

@pytest.mark.alembic
def test_view_create_pg(pytester):
    successful_test_run(pytester, count=1)

You’ll notice the name of the test must exactly match the name of the examples/ folder’s name.

For organization’s sake, examples’ names should look like test_<feature>_<test-purpose>_<dialect>/.

SQLAlchemy tests#

By contrast to the alembic tests, they dont need a special way to be invoked. One can just create a new tests/<feature>/test_<what-am-i-testing>.py and be done.

What still may be different relative to most projects though, is that most features of this library operate on a SQLAlchemy model-base/MetaData. So testing mulitple different scenarios within a singlar test file is frequently more challenging than it’s worth.

Therefore you’ll find that most test files contain exactly 1, or a couple of test. If a test file has more than one test, it’s frequently to test the same “thing” against the different dialect options.

For example:

import sqlalchemy
from pytest_mock_resources import create_postgres_fixture, create_sqlite_fixture

from sqlalchemy_declarative_extensions import (
    Schemas,
    declarative_database,
    register_sqlalchemy_events,
)
from sqlalchemy_declarative_extensions.sqlalchemy import declarative_base


_Base = declarative_base()

@declarative_database
class Base(_Base):  # type: ignore
    __abstract__ = True

    schemas = Schemas().are("fooschema")


class Foo(Base):
    __tablename__ = "foo"
    __table_args__ = {"schema": "fooschema"}

    id = sqlalchemy.Column(sqlalchemy.types.Integer(), primary_key=True)


register_sqlalchemy_events(Base.metadata, schemas=True)

pg = create_postgres_fixture(scope="function", engine_kwargs={"echo": True})
sqlite = create_sqlite_fixture()


def test_createall_schema_pg(pg):
    Base.metadata.create_all(bind=pg)
    with pg.connect() as conn:
        result = conn.execute(Foo.__table__.select()).fetchall()
    assert result == []


def test_createall_schema_sqlite(sqlite):
    Base.metadata.create_all(bind=sqlite, checkfirst=False)
    with sqlite.connect() as conn:
        result = conn.execute(Foo.__table__.select()).fetchall()
    assert result == []