Testing EllarSQL Models¶
There are various approaches to testing SQLAlchemy models, but in this section, we will focus on setting up a good testing environment for EllarSQL models using the Ellar Test factory and pytest.
For an effective testing environment, it is recommended to utilize the EllarSQLModule.register_setup()
approach to set up the EllarSQLModule. This allows you to add a new configuration for ELLAR_SQL
specific to your testing database, preventing interference with production or any other databases in use.
Defining TestConfig¶
There are various methods for configuring test settings in Ellar, as outlined here. However, in this section, we will adopt the 'in a file' approach.
Within the db_learning/config.py
file, include the following code:
import typing as t
...
class DevelopmentConfig(BaseConfig):
DEBUG: bool = True
# Configuration through Config
ELLAR_SQL: t.Dict[str, t.Any] = {
'databases': {
'default': 'sqlite:///project.db',
},
'echo': True,
'migration_options': {
'directory': 'migrations'
},
'models': ['models']
}
class TestConfig(BaseConfig):
DEBUG = False
ELLAR_SQL: t.Dict[str, t.Any] = {
**DevelopmentConfig.ELLAR_SQL,
'databases': {
'default': 'sqlite:///test.db',
},
'echo': False,
}
This snippet demonstrates the 'in a file' approach to setting up the TestConfig
class within the same db_learning/config.py
file.
Changes made:¶
- Updated the
databases
section to usesqlite+aiosqlite:///test.db
for the testing database. - Set
echo
toTrue
to enable SQLAlchemy output during testing for cleaner logs. - Preserved the
migration_options
andmodels
configurations fromDevelopmentConfig
.
Also, feel free to further adjust it based on your specific testing requirements!
Test Fixtures¶
After defining TestConfig
, we need to add some pytest fixtures to set up EllarSQLModule and another one that returns a session
for testing purposes. Additionally, we need to export ELLAR_CONFIG_MODULE
to point to the newly defined TestConfig.
import os
import pytest
from ellar.common.constants import ELLAR_CONFIG_MODULE
from ellar.testing import Test
from ellar_sql import EllarSQLService
from db_learning.root_module import ApplicationModule
# Setting the ELLAR_CONFIG_MODULE environment variable to TestConfig
os.environ.setdefault(ELLAR_CONFIG_MODULE, "db_learning.config:TestConfig")
# Fixture for creating a test module
@pytest.fixture(scope='session')
def tm():
test_module = Test.create_test_module(modules=[ApplicationModule])
yield test_module
# Fixture for creating a database session for testing
@pytest.fixture(scope='session')
def db(tm):
db_service = tm.get(EllarSQLService)
# Creating all tables
db_service.create_all()
yield
# Dropping all tables after the tests
db_service.drop_all()
# Fixture for creating a database session for testing
@pytest.fixture(scope='session')
def db_session(db, tm):
db_service = tm.get(EllarSQLService)
yield db_service.session_factory()
# Removing the session factory
db_service.session_factory.remove()
The provided fixtures help in setting up a testing environment for EllarSQL models. The Test.create_test_module
method creates a TestModule for initializing your Ellar application, and the db_session
fixture initializes a database session for testing, creating and dropping tables as needed.
If you are working with asynchronous database drivers, you can convert db_session
into an async function to handle coroutines seamlessly.
Alembic Migration with Test Fixture¶
In cases where there are already generated database migration files, and there is a need to apply migrations during testing, this can be achieved as shown in the example below:
import os
import pytest
from ellar.common.constants import ELLAR_CONFIG_MODULE
from ellar.testing import Test
from ellar_sql import EllarSQLService
from ellar_sql.cli.handlers import CLICommandHandlers
from db_learning.root_module import ApplicationModule
# Setting the ELLAR_CONFIG_MODULE environment variable to TestConfig
os.environ.setdefault(ELLAR_CONFIG_MODULE, "db_learning.config:TestConfig")
# Fixture for creating a test module
@pytest.fixture(scope='session')
def tm():
test_module = Test.create_test_module(modules=[ApplicationModule])
yield test_module
# Fixture for creating a database session for testing
@pytest.fixture(scope='session')
async def db(tm):
db_service = tm.get(EllarSQLService)
# Applying migrations using Alembic
async with tm.create_application().application_context():
cli = CLICommandHandlers(db_service)
cli.migrate()
yield
# Downgrading migrations after testing
async with tm.create_application().application_context():
cli = CLICommandHandlers(db_service)
cli.downgrade()
# Fixture for creating an asynchronous database session for testing
@pytest.fixture(scope='session')
async def db_session(db, tm):
db_service = tm.get(EllarSQLService)
yield db_service.session_factory()
# Removing the session factory
db_service.session_factory.remove()
The CLICommandHandlers
class wraps all Alembic
functions executed through the Ellar command-line interface. It can be used in conjunction with the application context to initialize all model tables during testing as shown in the illustration above. db_session
pytest fixture also ensures that migrations are applied and then downgraded after testing, maintaining a clean and consistent test database state.
Testing a Model¶
After setting up the testing database and creating a session, let's test the insertion of a user model into the database.
In db_learning/models.py
, we have a user model:
from ellar_sql import model
class User(model.Model):
id: model.Mapped[int] = model.mapped_column(model.Integer, primary_key=True)
username: model.Mapped[str] = model.mapped_column(model.String, unique=True, nullable=False)
email: model.Mapped[str] = model.mapped_column(model.String)
Now, create a file named test_user_model.py
:
import pytest
import sqlalchemy.exc as sa_exc
from db_learning.models import User
def test_username_must_be_unique(db_session):
# Creating and adding the first user
user1 = User(username='ellarSQL', email='ellarsql@gmail.com')
db_session.add(user1)
db_session.commit()
# Attempting to add a second user with the same username
user2 = User(username='ellarSQL', email='ellarsql2@gmail.com')
db_session.add(user2)
# Expecting an IntegrityError due to unique constraint violation
with pytest.raises(sa_exc.IntegrityError):
db_session.commit()
In this test, we are checking whether the unique constraint on the username
field is enforced by attempting to insert two users with the same username. The test expects an IntegrityError
to be raised, indicating a violation of the unique constraint. This ensures that the model behaves correctly and enforces the specified uniqueness requirement.
Testing Factory Boy¶
factory-boy provides a convenient and flexible way to create mock objects, supporting various ORMs like Django, MongoDB, and SQLAlchemy. EllarSQL extends factory.alchemy.SQLAlchemy
to offer a Model factory solution compatible with both synchronous and asynchronous database drivers.
To get started, you need to install factory-boy
:
pip install factory-boy
Now, let's create a factory for our user model in tests/factories.py
:
import factory
from db_learning.models import User
from ellar.app import current_injector
from sqlalchemy.orm import Session
from ellar_sql.factory import SESSION_PERSISTENCE_FLUSH, EllarSQLFactory
def _get_session():
session = current_injector.get(Session)
return session
class UserFactory(EllarSQLFactory):
class Meta:
model = User
sqlalchemy_session_persistence = SESSION_PERSISTENCE_FLUSH
sqlalchemy_session_factory = _get_session
username = factory.Faker('username')
email = factory.Faker('email')
The UserFactory
depends on a database Session as you see from _get_session()
function. We need to ensure that test fixture provides ApplicationContext
for current_injector
to work.
So in tests/conftest.py
, we make tm
test fixture to run application context:
import os
import pytest
from db_learning.root_module import ApplicationModule
from ellar.common.constants import ELLAR_CONFIG_MODULE
from ellar.testing import Test
from ellar.threading.sync_worker import execute_async_context_manager
from ellar_sql import EllarSQLService
os.environ.setdefault(ELLAR_CONFIG_MODULE, "db_learning.config:TestConfig")
@pytest.fixture(scope='session')
def tm():
test_module = Test.create_test_module(modules=[ApplicationModule])
app = test_module.create_application()
with execute_async_context_manager(app.application_context()):
yield test_module
# Fixture for creating a database session for testing
@pytest.fixture(scope='session')
def db(tm):
db_service = tm.get(EllarSQLService)
# Creating all tables
db_service.create_all()
yield
# Dropping all tables after the tests
db_service.drop_all()
With this setup, we can rewrite our test_username_must_be_unique
test using UserFactory
and factory_session
:
import pytest
import sqlalchemy.exc as sa_exc
from .factories import UserFactory
def test_username_must_be_unique(factory_session):
user1 = UserFactory()
with pytest.raises(sa_exc.IntegrityError):
UserFactory(username=user1.username)
This test yields the same result as before. Refer to the factory-boy documentation for more features and tutorials.