# **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](https://python-ellar.github.io/ellar/basics/testing/){target="_blank"} 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](https://python-ellar.github.io/ellar/basics/testing/#overriding-application-conf-during-testing){target="_blank"}.
However, in this section, we will adopt the 'in a file' approach.

Within the `db_learning/config.py` file, include the following code:

```python title="db_learning/config.py"
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:**
1. Updated the `databases` section to use `sqlite+aiosqlite:///test.db` for the testing database.
2. Set `echo` to `True` to enable SQLAlchemy output during testing for cleaner logs.
3. Preserved the `migration_options` and `models` configurations from `DevelopmentConfig`.

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**.

```python title="tests/conftest.py"
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:

```python title="tests/conftest.py"
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:

```python title="db_learning/model.py"
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`:

```python title="tests/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](https://pypi.org/project/factory-boy/){target="_blank"} 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`:

```shell
pip install factory-boy
```

Now, let's create a factory for our user model in `tests/factories.py`:

```python title="tests/factories.py"
import factory
from ellar_sql.factory import EllarSQLFactory, SESSION_PERSISTENCE_FLUSH
from db_learning.models import User
from . import common

class UserFactory(EllarSQLFactory):
    class Meta:
        model = User
        sqlalchemy_session_persistence = SESSION_PERSISTENCE_FLUSH
        sqlalchemy_session_factory = lambda: common.Session()

    username = factory.Faker('username')
    email = factory.Faker('email')
```

The `UserFactory` depends on a database session. Since the pytest fixture we created applies to it, 
we also need a session factory in `tests/common.py`:

```python title="tests/common.py"
from sqlalchemy import orm

Session = orm.scoped_session(orm.sessionmaker())
```

Additionally, we require a fixture responsible for configuring the Factory session in `tests/conftest.py`:

```python title="tests/conftest.py"
import os
import pytest
import sqlalchemy as sa
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
from . import common

os.environ.setdefault(ELLAR_CONFIG_MODULE, "db_learning.config:TestConfig")

@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()

@pytest.fixture
def factory_session(db, tm):
    engine = tm.get(sa.Engine)
    common.Session.configure(bind=engine)
    yield 
    common.Session.remove()
```

In the `factory_session` fixture, we retrieve the `Engine` registered in the DI container by **EllarSQLModule**. 
Using this engine, we configure the common `Session`. It's important to note that if you are using an 
async database driver, **EllarSQLModule** will register `AsyncEngine`.

With this setup, we can rewrite our `test_username_must_be_unique` test using `UserFactory` and `factory_session`:

```python title="tests/test_user_model.py"
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](https://factoryboy.readthedocs.io/en/stable/orms.html#sqlalchemy) 
for more features and tutorials.
