Skip to content

Commit ecb4954

Browse files
committed
Feat: Added Factory-Boy support and db_learning project source code
1 parent d323b73 commit ecb4954

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1859
-351
lines changed

Makefile

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ clean: ## Removing cached python compiled files
99
find . -name \*pyo | xargs rm -fv
1010
find . -name \*~ | xargs rm -fv
1111
find . -name __pycache__ | xargs rm -rfv
12+
find . -name .pytest_cache | xargs rm -rfv
1213
find . -name .ruff_cache | xargs rm -rfv
1314

1415
install: ## Install dependencies
@@ -23,14 +24,14 @@ lint:fmt ## Run code linters
2324
mypy ellar_sql
2425

2526
fmt format:clean ## Run code formatters
26-
ruff format ellar_sql tests
27-
ruff check --fix ellar_sql tests
27+
ruff format ellar_sql tests examples
28+
ruff check --fix ellar_sql tests examples
2829

29-
test: ## Run tests
30-
pytest tests
30+
test:clean ## Run tests
31+
pytest
3132

32-
test-cov: ## Run tests with coverage
33-
pytest --cov=ellar_sql --cov-report term-missing tests
33+
test-cov:clean ## Run tests with coverage
34+
pytest --cov=ellar_sql --cov-report term-missing
3435

3536
pre-commit-lint: ## Runs Requires commands during pre-commit
3637
make clean

README.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
[![PyPI version](https://img.shields.io/pypi/v/ellar-sql.svg)](https://pypi.python.org/pypi/ellar-sql)
99
[![PyPI version](https://img.shields.io/pypi/pyversions/ellar-sql.svg)](https://pypi.python.org/pypi/ellar-sql)
1010

11-
## Project Status
12-
- [x] Production Ready
13-
- [ ] SQLAlchemy Django Like Query
1411

1512
## Introduction
1613
EllarSQL Module adds support for `SQLAlchemy` and `Alembic` package to your Ellar application

docs/index.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,10 @@ EllarSQL comes packed with a set of awesome features designed:
3737
## **Requirements**
3838
EllarSQL core dependencies includes:
3939

40-
- Python version >= 3.8
41-
- Ellar Framework >= 0.6.7
42-
- SQLAlchemy ORM >= 2.0.16
40+
- Python >= 3.8
41+
- Ellar >= 0.6.7
42+
- SQLAlchemy >= 2.0.16
4343
- Alembic >= 1.10.0
44-
- Pillow >= 10.1.0
45-
- Python-Magic >= 0.4.27
4644

4745
## **Installation**
4846

docs/pagination/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ from .models import User
4848

4949

5050
class UserSchema(ec.Serializer):
51-
id: str
52-
name: str
53-
fullname: str
51+
id: int
52+
username: str
53+
email: str
5454

5555

5656
@ec.get('/users')

docs/testing/index.md

Lines changed: 305 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,311 @@
1+
# **Testing EllarSQL Models**
2+
There are various approaches to testing SQLAlchemy models, but in this section, we will focus on setting
3+
up a good testing environment for EllarSQL models using the
4+
Ellar [Test](https://python-ellar.github.io/ellar/basics/testing/){target="_blank"} factory and pytest.
15

6+
For an effective testing environment, it is recommended to utilize the `EllarSQLModule.register_setup()`
7+
approach to set up the **EllarSQLModule**. This allows you to add a new configuration for `ELLAR_SQL`
8+
specific to your testing database, preventing interference with production or any other databases in use.
29

10+
### **Defining TestConfig**
11+
There are various methods for configuring test settings in Ellar,
12+
as outlined
13+
[here](https://python-ellar.github.io/ellar/basics/testing/#overriding-application-conf-during-testing){target="_blank"}.
14+
However, in this section, we will adopt the 'in a file' approach.
315

4-
## Testing Fixtures
16+
Within the `db_learning/config.py` file, include the following code:
517

6-
## Alembic Migration with Test Fixture
18+
```python title="db_learning/config.py"
19+
import typing as t
20+
...
721

8-
## Testing a model
22+
class DevelopmentConfig(BaseConfig):
23+
DEBUG: bool = True
24+
# Configuration through Config
25+
ELLAR_SQL: t.Dict[str, t.Any] = {
26+
'databases': {
27+
'default': 'sqlite:///project.db',
28+
},
29+
'echo': True,
30+
'migration_options': {
31+
'directory': 'migrations'
32+
},
33+
'models': ['models']
34+
}
935

10-
## Factory Boy
36+
class TestConfig(BaseConfig):
37+
DEBUG = False
38+
39+
ELLAR_SQL: t.Dict[str, t.Any] = {
40+
**DevelopmentConfig.ELLAR_SQL,
41+
'databases': {
42+
'default': 'sqlite:///test.db',
43+
},
44+
'echo': False,
45+
}
46+
```
47+
48+
This snippet demonstrates the 'in a file' approach to setting up the `TestConfig` class within the same `db_learning/config.py` file.
49+
50+
#### **Changes made:**
51+
1. Updated the `databases` section to use `sqlite+aiosqlite:///test.db` for the testing database.
52+
2. Set `echo` to `True` to enable SQLAlchemy output during testing for cleaner logs.
53+
3. Preserved the `migration_options` and `models` configurations from `DevelopmentConfig`.
54+
55+
Also, feel free to further adjust it based on your specific testing requirements!
56+
57+
## **Test Fixtures**
58+
After defining `TestConfig`, we need to add some pytest fixtures to set up **EllarSQLModule** and another one
59+
that returns a `session` for testing purposes. Additionally, we need to export `ELLAR_CONFIG_MODULE`
60+
to point to the newly defined **TestConfig**.
61+
62+
```python title="tests/conftest.py"
63+
import os
64+
import pytest
65+
from ellar.common.constants import ELLAR_CONFIG_MODULE
66+
from ellar.testing import Test
67+
from ellar_sql import EllarSQLService
68+
from db_learning.root_module import ApplicationModule
69+
70+
# Setting the ELLAR_CONFIG_MODULE environment variable to TestConfig
71+
os.environ.setdefault(ELLAR_CONFIG_MODULE, "db_learning.config:TestConfig")
72+
73+
# Fixture for creating a test module
74+
@pytest.fixture(scope='session')
75+
def tm():
76+
test_module = Test.create_test_module(modules=[ApplicationModule])
77+
yield test_module
78+
79+
# Fixture for creating a database session for testing
80+
@pytest.fixture(scope='session')
81+
def db(tm):
82+
db_service = tm.get(EllarSQLService)
83+
84+
# Creating all tables
85+
db_service.create_all()
86+
87+
yield
88+
89+
# Dropping all tables after the tests
90+
db_service.drop_all()
91+
92+
# Fixture for creating a database session for testing
93+
@pytest.fixture(scope='session')
94+
def db_session(db, tm):
95+
db_service = tm.get(EllarSQLService)
96+
97+
yield db_service.session_factory()
98+
99+
# Removing the session factory
100+
db_service.session_factory.remove()
101+
```
102+
103+
The provided fixtures help in setting up a testing environment for EllarSQL models.
104+
The `Test.create_test_module` method creates a **TestModule** for initializing your Ellar application,
105+
and the `db_session` fixture initializes a database session for testing, creating and dropping tables as needed.
106+
107+
If you are working with asynchronous database drivers, you can convert `db_session`
108+
into an async function to handle coroutines seamlessly.
109+
110+
## **Alembic Migration with Test Fixture**
111+
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:
112+
113+
```python title="tests/conftest.py"
114+
import os
115+
import pytest
116+
from ellar.common.constants import ELLAR_CONFIG_MODULE
117+
from ellar.testing import Test
118+
from ellar_sql import EllarSQLService
119+
from ellar_sql.cli.handlers import CLICommandHandlers
120+
from db_learning.root_module import ApplicationModule
121+
122+
# Setting the ELLAR_CONFIG_MODULE environment variable to TestConfig
123+
os.environ.setdefault(ELLAR_CONFIG_MODULE, "db_learning.config:TestConfig")
124+
125+
# Fixture for creating a test module
126+
@pytest.fixture(scope='session')
127+
def tm():
128+
test_module = Test.create_test_module(modules=[ApplicationModule])
129+
yield test_module
130+
131+
132+
# Fixture for creating a database session for testing
133+
@pytest.fixture(scope='session')
134+
async def db(tm):
135+
db_service = tm.get(EllarSQLService)
136+
137+
# Applying migrations using Alembic
138+
async with tm.create_application().application_context():
139+
cli = CLICommandHandlers(db_service)
140+
cli.migrate()
141+
142+
yield
143+
144+
# Downgrading migrations after testing
145+
async with tm.create_application().application_context():
146+
cli = CLICommandHandlers(db_service)
147+
cli.downgrade()
148+
149+
# Fixture for creating an asynchronous database session for testing
150+
@pytest.fixture(scope='session')
151+
async def db_session(db, tm):
152+
db_service = tm.get(EllarSQLService)
153+
154+
yield db_service.session_factory()
155+
156+
# Removing the session factory
157+
db_service.session_factory.remove()
158+
```
159+
160+
The `CLICommandHandlers` class wraps all `Alembic` functions executed through the Ellar command-line interface.
161+
It can be used in conjunction with the application context to initialize all model tables during testing as shown in the illustration above.
162+
`db_session` pytest fixture also ensures that migrations are applied and then downgraded after testing,
163+
maintaining a clean and consistent test database state.
164+
165+
## **Testing a Model**
166+
After setting up the testing database and creating a session, let's test the insertion of a user model into the database.
167+
168+
In `db_learning/models.py`, we have a user model:
169+
170+
```python title="db_learning/model.py"
171+
from ellar_sql import model
172+
173+
class User(model.Model):
174+
id: model.Mapped[int] = model.mapped_column(model.Integer, primary_key=True)
175+
username: model.Mapped[str] = model.mapped_column(model.String, unique=True, nullable=False)
176+
email: model.Mapped[str] = model.mapped_column(model.String)
177+
```
178+
179+
Now, create a file named `test_user_model.py`:
180+
181+
```python title="tests/test_user_model.py"
182+
import pytest
183+
import sqlalchemy.exc as sa_exc
184+
from db_learning.models import User
185+
186+
def test_username_must_be_unique(db_session):
187+
# Creating and adding the first user
188+
user1 = User(username='ellarSQL', email='ellarsql@gmail.com')
189+
db_session.add(user1)
190+
db_session.commit()
191+
192+
# Attempting to add a second user with the same username
193+
user2 = User(username='ellarSQL', email='ellarsql2@gmail.com')
194+
db_session.add(user2)
195+
196+
# Expecting an IntegrityError due to unique constraint violation
197+
with pytest.raises(sa_exc.IntegrityError):
198+
db_session.commit()
199+
```
200+
201+
In this test, we are checking whether the unique constraint on the `username`
202+
field is enforced by attempting to insert two users with the same username.
203+
The test expects an `IntegrityError` to be raised, indicating a violation of the unique constraint.
204+
This ensures that the model behaves correctly and enforces the specified uniqueness requirement.
205+
206+
## **Testing Factory Boy**
207+
[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.
208+
209+
To get started, you need to install `factory-boy`:
210+
211+
```shell
212+
pip install factory-boy
213+
```
214+
215+
Now, let's create a factory for our user model in `tests/factories.py`:
216+
217+
```python title="tests/factories.py"
218+
import factory
219+
from ellar_sql.factory import EllarSQLFactory, SESSION_PERSISTENCE_FLUSH
220+
from db_learning.models import User
221+
from . import common
222+
223+
class UserFactory(EllarSQLFactory):
224+
class Meta:
225+
model = User
226+
sqlalchemy_session_persistence = SESSION_PERSISTENCE_FLUSH
227+
sqlalchemy_session_factory = lambda: common.Session()
228+
229+
username = factory.Faker('username')
230+
email = factory.Faker('email')
231+
```
232+
233+
The `UserFactory` depends on a database session. Since the pytest fixture we created applies to it,
234+
we also need a session factory in `tests/common.py`:
235+
236+
```python title="tests/common.py"
237+
from sqlalchemy import orm
238+
239+
Session = orm.scoped_session(orm.sessionmaker())
240+
```
241+
242+
Additionally, we require a fixture responsible for configuring the Factory session in `tests/conftest.py`:
243+
244+
```python title="tests/conftest.py"
245+
import os
246+
import pytest
247+
import sqlalchemy as sa
248+
from ellar.common.constants import ELLAR_CONFIG_MODULE
249+
from ellar.testing import Test
250+
from ellar_sql import EllarSQLService
251+
from db_learning.root_module import ApplicationModule
252+
from . import common
253+
254+
os.environ.setdefault(ELLAR_CONFIG_MODULE, "db_learning.config:TestConfig")
255+
256+
@pytest.fixture(scope='session')
257+
def tm():
258+
test_module = Test.create_test_module(modules=[ApplicationModule])
259+
yield test_module
260+
261+
# Fixture for creating a database session for testing
262+
@pytest.fixture(scope='session')
263+
def db(tm):
264+
db_service = tm.get(EllarSQLService)
265+
266+
# Creating all tables
267+
db_service.create_all()
268+
269+
yield
270+
271+
# Dropping all tables after the tests
272+
db_service.drop_all()
273+
274+
# Fixture for creating a database session for testing
275+
@pytest.fixture(scope='session')
276+
def db_session(db, tm):
277+
db_service = tm.get(EllarSQLService)
278+
279+
yield db_service.session_factory()
280+
281+
# Removing the session factory
282+
db_service.session_factory.remove()
283+
284+
@pytest.fixture
285+
def factory_session(db, tm):
286+
engine = tm.get(sa.Engine)
287+
common.Session.configure(bind=engine)
288+
yield
289+
common.Session.remove()
290+
```
291+
292+
In the `factory_session` fixture, we retrieve the `Engine` registered in the DI container by **EllarSQLModule**.
293+
Using this engine, we configure the common `Session`. It's important to note that if you are using an
294+
async database driver, **EllarSQLModule** will register `AsyncEngine`.
295+
296+
With this setup, we can rewrite our `test_username_must_be_unique` test using `UserFactory` and `factory_session`:
297+
298+
```python title="tests/test_user_model.py"
299+
import pytest
300+
import sqlalchemy.exc as sa_exc
301+
from .factories import UserFactory
302+
303+
def test_username_must_be_unique(factory_session):
304+
user1 = UserFactory()
305+
with pytest.raises(sa_exc.IntegrityError):
306+
UserFactory(username=user1.username)
307+
```
308+
309+
This test yields the same result as before.
310+
Refer to the [factory-boy documentation](https://factoryboy.readthedocs.io/en/stable/orms.html#sqlalchemy)
311+
for more features and tutorials.

0 commit comments

Comments
 (0)
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