Skip to content

Commit b439aab

Browse files
committed
test utils
1 parent 4ba6184 commit b439aab

File tree

8 files changed

+101
-46
lines changed

8 files changed

+101
-46
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,19 @@ replica = DBConnect(
265265
before_create_session_handler=renew_replica_connect,
266266
)
267267
```
268+
269+
## Testing
270+
271+
The library provides several ready-made utils that can be used in tests,
272+
for example in fixtures. It helps write tests that share a common transaction
273+
between the test and the application, so data isolation between tests is
274+
achieved through fast transaction rollback.
275+
276+
277+
You can see the capabilities in the examples:
278+
279+
[Here are tests with a common transaction between the
280+
application and the tests.](https://github.com/krylosov-aa/context-async-sqlalchemy/blob/main/exmaples/fastapi_example/tests/transactional/__init__.py)
281+
282+
283+
[And here's an example with different transactions.](https://github.com/krylosov-aa/context-async-sqlalchemy/blob/main/exmaples/fastapi_example/tests/non_transactional/__init__.py)

context_async_sqlalchemy/context.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from sqlalchemy.ext.asyncio import AsyncSession
55

6+
from .connect import DBConnect
7+
68

79
def init_db_session_ctx() -> Token[dict[str, AsyncSession] | None]:
810
"""
@@ -55,14 +57,14 @@ def get_db_session_from_context(context_key: str) -> AsyncSession | None:
5557

5658

5759
def put_db_session_to_context(
58-
context_key: str,
60+
connection: DBConnect,
5961
session: AsyncSession,
6062
) -> None:
6163
"""
6264
Puts the session into context
6365
"""
6466
session_ctx = _get_initiated_context()
65-
session_ctx[context_key] = session
67+
session_ctx[connection.context_key] = session
6668

6769

6870
def sessions_stream() -> Generator[AsyncSession, Any, None]:

context_async_sqlalchemy/session.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async def db_session(connect: DBConnect) -> AsyncSession:
2222
session = get_db_session_from_context(connect.context_key)
2323
if not session:
2424
session = await connect.create_session()
25-
put_db_session_to_context(connect.context_key, session)
25+
put_db_session_to_context(connect, session)
2626
return session
2727

2828

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Utilities to use during testing"""
2+
3+
from contextlib import asynccontextmanager
4+
from typing import AsyncGenerator
5+
6+
from sqlalchemy.ext.asyncio import AsyncSession
7+
8+
from context_async_sqlalchemy import (
9+
DBConnect,
10+
init_db_session_ctx,
11+
put_db_session_to_context,
12+
reset_db_session_ctx,
13+
)
14+
15+
16+
@asynccontextmanager
17+
async def rollback_session(
18+
connection: DBConnect,
19+
) -> AsyncGenerator[AsyncSession]:
20+
"""A session that always rolls back"""
21+
session_maker = await connection.get_session_maker()
22+
async with session_maker() as session:
23+
try:
24+
yield session
25+
finally:
26+
await session.rollback()
27+
28+
29+
@asynccontextmanager
30+
async def set_test_context() -> AsyncGenerator[None]:
31+
"""
32+
Opens a context similar to middleware, but doesn't commit or
33+
rollback automatically. This task falls to the fixture in tests.
34+
"""
35+
token = init_db_session_ctx()
36+
try:
37+
yield
38+
finally:
39+
await reset_db_session_ctx(
40+
token,
41+
# Don't close the session here, as you opened in fixture.
42+
with_close=False,
43+
)
44+
45+
46+
@asynccontextmanager
47+
async def put_savepoint_session_in_ctx(
48+
connection: DBConnect,
49+
session: AsyncSession,
50+
) -> AsyncGenerator[None]:
51+
"""
52+
Sets the context to a session that uses a save point instead of creating
53+
a transaction. You need to pass the session you're using inside
54+
your tests to attach a new session to the same connection.
55+
56+
It is also important to use this function inside set_test_context.
57+
"""
58+
session_maker = await connection.get_session_maker()
59+
async with session_maker(
60+
# Bind to the same connection
61+
bind=await session.connection(),
62+
# Instead of opening a transaction, it creates a save point
63+
join_transaction_mode="create_savepoint",
64+
) as session:
65+
put_db_session_to_context(connection, session)
66+
yield

exmaples/fastapi_example/tests/conftest.py

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import pytest_asyncio
88
from fastapi import FastAPI
99
from httpx import AsyncClient, ASGITransport
10-
from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession
10+
from sqlalchemy.ext.asyncio import AsyncSession
1111

12+
from context_async_sqlalchemy.test_utils import rollback_session
1213
from exmaples.fastapi_example.database import master
1314
from exmaples.fastapi_example.setup_app import lifespan, setup_app
1415

@@ -34,20 +35,7 @@ async def client(app: FastAPI) -> AsyncGenerator[AsyncClient]:
3435

3536

3637
@pytest_asyncio.fixture
37-
async def db_session_test(
38-
session_maker_test: async_sessionmaker[AsyncSession],
39-
) -> AsyncGenerator[AsyncSession]:
38+
async def db_session_test() -> AsyncGenerator[AsyncSession]:
4039
"""The session that is used inside the test"""
41-
async with session_maker_test() as session:
42-
try:
43-
yield session
44-
finally:
45-
await session.rollback()
46-
47-
48-
@pytest_asyncio.fixture
49-
async def session_maker_test(
50-
app: FastAPI, # To make the connection to the database in lifespan
51-
) -> AsyncGenerator[async_sessionmaker[AsyncSession]]:
52-
session_maker = await master.get_session_maker()
53-
yield session_maker
40+
async with rollback_session(master) as session:
41+
yield session

exmaples/fastapi_example/tests/transactional/conftest.py

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@
1313
from typing import AsyncGenerator
1414

1515
import pytest_asyncio
16-
1716
from sqlalchemy.ext.asyncio import AsyncSession
1817

19-
from exmaples.fastapi_example.database import master
20-
from context_async_sqlalchemy import (
21-
init_db_session_ctx,
22-
put_db_session_to_context,
23-
reset_db_session_ctx,
18+
from context_async_sqlalchemy.test_utils import (
19+
put_savepoint_session_in_ctx,
20+
set_test_context,
2421
)
22+
from exmaples.fastapi_example.database import master
2523

2624

2725
@pytest_asyncio.fixture(autouse=True)
@@ -33,21 +31,6 @@ async def db_session_override(
3331
The middleware has a special check that won't initialize the context
3432
if it already exists.
3533
"""
36-
token = init_db_session_ctx()
37-
38-
# Here we create a new session with save point behavior.
39-
# This means that committing within the application will save and
40-
# release the save point, rather than committing the entire transaction.
41-
conn = await db_session_test.connection()
42-
new_session = AsyncSession(
43-
bind=conn, join_transaction_mode="create_savepoint"
44-
)
45-
put_db_session_to_context(master.context_key, new_session)
46-
try:
47-
yield
48-
finally:
49-
await reset_db_session_ctx(
50-
token,
51-
# Don't close the session here, as you opened in fixture.
52-
with_close=False,
53-
)
34+
async with set_test_context():
35+
async with put_savepoint_session_in_ctx(master, db_session_test):
36+
yield

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "context-async-sqlalchemy"
3-
version = "1.2.1"
3+
version = "1.2.2"
44
description = "A convenient way to configure and interact with a async sqlalchemy session through context in asynchronous applications"
55
readme = "README.md"
66
authors = [

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)