Skip to content

Documentation and Code refactor #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jan 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Feat: Added Factory-Boy support and db_learning project source code
  • Loading branch information
eadwinCode committed Jan 27, 2024
commit ecb4954c4ddda21f8b6e2063b11b73a7d1797b4e
13 changes: 7 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ clean: ## Removing cached python compiled files
find . -name \*pyo | xargs rm -fv
find . -name \*~ | xargs rm -fv
find . -name __pycache__ | xargs rm -rfv
find . -name .pytest_cache | xargs rm -rfv
find . -name .ruff_cache | xargs rm -rfv

install: ## Install dependencies
Expand All @@ -23,14 +24,14 @@ lint:fmt ## Run code linters
mypy ellar_sql

fmt format:clean ## Run code formatters
ruff format ellar_sql tests
ruff check --fix ellar_sql tests
ruff format ellar_sql tests examples
ruff check --fix ellar_sql tests examples

test: ## Run tests
pytest tests
test:clean ## Run tests
pytest

test-cov: ## Run tests with coverage
pytest --cov=ellar_sql --cov-report term-missing tests
test-cov:clean ## Run tests with coverage
pytest --cov=ellar_sql --cov-report term-missing

pre-commit-lint: ## Runs Requires commands during pre-commit
make clean
Expand Down
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
[![PyPI version](https://img.shields.io/pypi/v/ellar-sql.svg)](https://pypi.python.org/pypi/ellar-sql)
[![PyPI version](https://img.shields.io/pypi/pyversions/ellar-sql.svg)](https://pypi.python.org/pypi/ellar-sql)

## Project Status
- [x] Production Ready
- [ ] SQLAlchemy Django Like Query

## Introduction
EllarSQL Module adds support for `SQLAlchemy` and `Alembic` package to your Ellar application
Expand Down
8 changes: 3 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,10 @@ EllarSQL comes packed with a set of awesome features designed:
## **Requirements**
EllarSQL core dependencies includes:

- Python version >= 3.8
- Ellar Framework >= 0.6.7
- SQLAlchemy ORM >= 2.0.16
- Python >= 3.8
- Ellar >= 0.6.7
- SQLAlchemy >= 2.0.16
- Alembic >= 1.10.0
- Pillow >= 10.1.0
- Python-Magic >= 0.4.27

## **Installation**

Expand Down
6 changes: 3 additions & 3 deletions docs/pagination/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ from .models import User


class UserSchema(ec.Serializer):
id: str
name: str
fullname: str
id: int
username: str
email: str


@ec.get('/users')
Expand Down
309 changes: 305 additions & 4 deletions docs/testing/index.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,311 @@
# **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.

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

## Alembic Migration with Test Fixture
```python title="db_learning/config.py"
import typing as t
...

## Testing a model
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']
}

## Factory Boy
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.
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy