Skip to content

Commit ff04943

Browse files
authored
Merge pull request #11 from python-ellar/factory_boy
Factory boy Support
2 parents cf2d0ea + 17390ec commit ff04943

Some content is hidden

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

50 files changed

+1604
-97
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='[email protected]')
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='[email protected]')
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)