Basic testing infrastructure

This commit is contained in:
Eden Kirin
2023-09-20 09:56:52 +02:00
parent f57c4d4491
commit 6109630ed1
12 changed files with 451 additions and 22 deletions

View File

@ -91,6 +91,7 @@ class TestingSettings(BaseEnvSettings):
DB_HOST: str = "localhost" DB_HOST: str = "localhost"
DB_PORT: int = 5432 DB_PORT: int = 5432
DB_TEMPLATE_NAME: str = "db-template-name"
DB_NAME: str = "test_db-name" DB_NAME: str = "test_db-name"
DB_USER: str = "db-user" DB_USER: str = "db-user"
DB_PASSWORD: str = "db-password" DB_PASSWORD: str = "db-password"

View File

@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, cast from typing import TYPE_CHECKING, cast
from uuid import UUID from uuid import UUID
@ -13,7 +14,12 @@ from litestar.contrib.sqlalchemy.plugins.init.config.common import (
) )
from litestar.utils import delete_litestar_scope_state, get_litestar_scope_state from litestar.utils import delete_litestar_scope_state, get_litestar_scope_state
from sqlalchemy import event from sqlalchemy import event
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)
from sqlalchemy.pool import NullPool from sqlalchemy.pool import NullPool
from app.lib import settings from app.lib import settings
@ -37,16 +43,25 @@ def _default(val: Any) -> str:
raise TypeError() raise TypeError()
db_connection_url = sqlalchemy.engine.URL.create( @dataclass
drivername="postgresql+asyncpg", class DBConnectionSettings:
username=settings.db.USER, username: str
password=settings.db.PASSWORD, password: str
host=settings.db.HOST, host: str
port=settings.db.PORT, port: int
database=settings.db.NAME, database: str
)
engine = create_async_engine(
def create_db_engine(connection_settings: DBConnectionSettings) -> AsyncEngine:
db_connection_url = sqlalchemy.engine.URL.create(
drivername="postgresql+asyncpg",
username=connection_settings.username,
password=connection_settings.password,
host=connection_settings.host,
port=connection_settings.port,
database=connection_settings.database,
)
return create_async_engine(
db_connection_url, db_connection_url,
echo=settings.db.ECHO, echo=settings.db.ECHO,
echo_pool=settings.db.ECHO_POOL, echo_pool=settings.db.ECHO_POOL,
@ -55,7 +70,20 @@ engine = create_async_engine(
pool_size=settings.db.POOL_SIZE, pool_size=settings.db.POOL_SIZE,
pool_timeout=settings.db.POOL_TIMEOUT, pool_timeout=settings.db.POOL_TIMEOUT,
poolclass=NullPool if settings.db.POOL_DISABLE else None, poolclass=NullPool if settings.db.POOL_DISABLE else None,
)
engine = create_db_engine(
connection_settings=DBConnectionSettings(
username=settings.db.USER,
password=settings.db.PASSWORD,
host=settings.db.HOST,
port=settings.db.PORT,
database=settings.db.NAME,
)
) )
"""Configure via DatabaseSettings. """Configure via DatabaseSettings.
Overrides default JSON Overrides default JSON

View File

View File

@ -0,0 +1,107 @@
from asyncio import current_task
from typing import AsyncGenerator, Callable, Generator
import pytest
from _pytest.fixtures import FixtureRequest
from _pytest.tmpdir import TempPathFactory
from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_scoped_session
from sqlalchemy.orm import sessionmaker
import settings_test
from televend_core.databases.televend_repositories.mapper import start_televend_mappers
from televend_core.databases.televend_repositories.repository import (
TelevendRepositoryManager,
)
from televend_core.test_extras import factory_boy_utils
from televend_core.test_extras.database_utils import (
DatabaseConfig,
configure_database,
generate_async_engine,
generate_test_database_name,
xdist_lock_pytest,
)
TELEVEND_TEMPLATE_DATABASE_CONFIG = DatabaseConfig(
engine="postgresql",
user=settings_test.POSTGRES_TEST_USER,
password=settings_test.POSTGRES_TEST_PASSWORD,
name="test_televend",
host=settings_test.POSTGRES_HOST,
port=settings_test.POSTGRES_PORT,
)
@pytest.fixture(scope="session")
def televend_fixture_setup(worker_id: int, tmp_path_factory: TempPathFactory) -> None:
start_televend_mappers("selectin")
# get the temp directory shared by all workers
# When xdist is disabled (running with -n0 for example), then worker_id will return "master"
if worker_id == "master":
root_tmp_dir = tmp_path_factory.getbasetemp()
else:
root_tmp_dir = tmp_path_factory.getbasetemp().parent
file_path = root_tmp_dir / "televend_fixture.txt"
lock_path = file_path.with_suffix(".lock")
xdist_lock_pytest(
file_path=file_path,
lock_path=lock_path,
fn=lambda: configure_database(
connection_string=TELEVEND_TEMPLATE_DATABASE_CONFIG.to_connection_string(),
database_type="cloud",
),
)
@pytest.fixture(scope="function")
def televend_test_database_name(request: FixtureRequest) -> Generator[str, None, None]:
test_database_name = generate_test_database_name(test_name=request.node.originalname)
yield test_database_name
@pytest.fixture(scope="function")
async def async_televend_engine(
televend_fixture_setup: Callable, televend_test_database_name: str
) -> AsyncGenerator[AsyncEngine, None]:
async for async_engine in generate_async_engine(
template_database_config=TELEVEND_TEMPLATE_DATABASE_CONFIG,
test_database_name=televend_test_database_name,
):
yield async_engine
@pytest.fixture(scope="function")
async def async_televend_session(
async_televend_engine: AsyncEngine,
) -> AsyncGenerator[AsyncSession, None]:
# Prepare a new, clean async_session, one sync async_session for factory boy and one async for normal usage
sync_engine = create_engine(
url=async_televend_engine.url.set(drivername="postgresql"), pool_size=1, max_overflow=1
)
factory_boy_utils.TelevendSession.configure(bind=sync_engine)
televend_async_session_factory = sessionmaker(
bind=async_televend_engine, expire_on_commit=False, class_=AsyncSession
)
televend_async_session = async_scoped_session(
televend_async_session_factory, scopefunc=current_task
)
session = televend_async_session()
yield session
factory_boy_utils.TelevendSession.remove()
await session.rollback() # to avoid coroutine 'Transaction.rollback' was never awaited warning
await televend_async_session.remove()
@pytest.fixture(scope="function")
def televend_repository_manager(
async_televend_session: AsyncSession, async_televend_engine: AsyncEngine
) -> TelevendRepositoryManager:
return TelevendRepositoryManager(
async_session=async_televend_session,
async_engine=async_televend_engine,
)

View File

@ -0,0 +1,93 @@
from typing import Protocol
import asyncpg
import sqlalchemy
from asyncpg import Connection, DuplicateDatabaseError, InvalidCatalogNameError
from migrate import DatabaseConfig, migrate
class TestingSettingsInitOptions(Protocol):
DB_HOST: str
DB_PORT: int
DB_TEMPLATE_NAME: str
DB_NAME: str
DB_USER: str
DB_PASSWORD: str
DROP_DATABASE_BEFORE_TESTS: bool
DROP_DATABASE_AFTER_TESTS: bool
class TestingDatabaseSetup:
def __init__(self, options: TestingSettingsInitOptions):
self.options = options
db_connection_url = sqlalchemy.engine.URL.create(
drivername="postgresql",
username=self.options.DB_USER,
password=self.options.DB_PASSWORD,
host=self.options.DB_HOST,
port=self.options.DB_PORT,
)
self.connection_str = db_connection_url.render_as_string(hide_password=False)
async def _create_template_db(self, conn: Connection):
query = f"CREATE DATABASE {self.options.DB_TEMPLATE_NAME}"
try:
await conn.execute(query)
except DuplicateDatabaseError:
...
async def _drop_template_db(self, conn: Connection):
query = f"DROP DATABASE {self.options.DB_TEMPLATE_NAME}"
try:
await conn.execute(query)
except InvalidCatalogNameError:
...
async def _create_test_db(self, conn: Connection):
query = f"CREATE DATABASE {self.options.DB_NAME} TEMPLATE {self.options.DB_TEMPLATE_NAME}"
try:
await conn.execute(query)
except DuplicateDatabaseError:
...
async def _drop_test_db(self, conn: Connection):
query = f"DROP DATABASE {self.options.DB_NAME}"
try:
await conn.execute(query)
except InvalidCatalogNameError:
...
def _migrate_template_database(self):
conf = DatabaseConfig(
HOST=self.options.DB_HOST,
PORT=self.options.DB_PORT,
NAME=self.options.DB_TEMPLATE_NAME,
USER=self.options.DB_USER,
PASSWORD=self.options.DB_PASSWORD,
)
migrate(conf)
async def init_db(self):
conn = await asyncpg.connect(self.connection_str, database="postgres")
if self.options.DROP_DATABASE_BEFORE_TESTS:
await self._drop_template_db(conn)
await self._create_template_db(conn)
await self._create_test_db(conn)
await conn.close()
if self.options.DROP_DATABASE_BEFORE_TESTS:
self._migrate_template_database()
async def tear_down_db(self):
conn = await asyncpg.connect(self.connection_str, database="postgres")
await self._drop_test_db(conn)
if self.options.DROP_DATABASE_AFTER_TESTS:
await self._drop_template_db(conn)
await conn.close()

View File

@ -1,5 +1,6 @@
import subprocess import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from app.lib import settings from app.lib import settings
@ -14,13 +15,18 @@ class DatabaseConfig:
def migrate(conf: DatabaseConfig) -> None: def migrate(conf: DatabaseConfig) -> None:
script_path = Path(__file__).parent
migrations_path = script_path / "migrations"
conf_file = migrations_path / "flyway.conf"
args = [ args = [
settings.db.FLYWAY_PATH, settings.db.FLYWAY_PATH,
"migrate", "migrate",
f"-url=jdbc:postgresql://{conf.HOST}:{conf.PORT}/{conf.NAME}", f"-url=jdbc:postgresql://{conf.HOST}:{conf.PORT}/{conf.NAME}",
f"-user={conf.USER}", f"-user={conf.USER}",
f"-password={conf.PASSWORD}", f"-password={conf.PASSWORD}",
"-configFiles=migrations/flyway.conf", f"-configFiles={conf_file}",
f"-locations=filesystem:{migrations_path}",
] ]
subprocess.run( subprocess.run(

38
poetry.lock generated
View File

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. # This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
[[package]] [[package]]
name = "annotated-types" name = "annotated-types"
@ -171,6 +171,7 @@ files = [
{file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"}, {file = "greenlet-2.0.2-cp27-cp27m-win32.whl", hash = "sha256:6c3acb79b0bfd4fe733dff8bc62695283b57949ebcca05ae5c129eb606ff2d74"},
{file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"}, {file = "greenlet-2.0.2-cp27-cp27m-win_amd64.whl", hash = "sha256:283737e0da3f08bd637b5ad058507e578dd462db259f7f6e4c5c365ba4ee9343"},
{file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"}, {file = "greenlet-2.0.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:d27ec7509b9c18b6d73f2f5ede2622441de812e7b1a80bbd446cb0633bd3d5ae"},
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d967650d3f56af314b72df7089d96cda1083a7fc2da05b375d2bc48c82ab3f3c"},
{file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"}, {file = "greenlet-2.0.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:30bcf80dda7f15ac77ba5af2b961bdd9dbc77fd4ac6105cee85b0d0a5fcf74df"},
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26fbfce90728d82bc9e6c38ea4d038cba20b7faf8a0ca53a9c07b67318d46088"},
{file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"}, {file = "greenlet-2.0.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9190f09060ea4debddd24665d6804b995a9c122ef5917ab26e1566dcc712ceeb"},
@ -179,6 +180,7 @@ files = [
{file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"}, {file = "greenlet-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:76ae285c8104046b3a7f06b42f29c7b73f77683df18c49ab5af7983994c2dd91"},
{file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"}, {file = "greenlet-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:2d4686f195e32d36b4d7cf2d166857dbd0ee9f3d20ae349b6bf8afc8485b3645"},
{file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"}, {file = "greenlet-2.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c4302695ad8027363e96311df24ee28978162cdcdd2006476c43970b384a244c"},
{file = "greenlet-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d4606a527e30548153be1a9f155f4e283d109ffba663a15856089fb55f933e47"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c48f54ef8e05f04d6eff74b8233f6063cb1ed960243eacc474ee73a2ea8573ca"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a1846f1b999e78e13837c93c778dcfc3365902cfb8d1bdb7dd73ead37059f0d0"},
{file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"}, {file = "greenlet-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a06ad5312349fec0ab944664b01d26f8d1f05009566339ac6f63f56589bc1a2"},
@ -208,6 +210,7 @@ files = [
{file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"}, {file = "greenlet-2.0.2-cp37-cp37m-win32.whl", hash = "sha256:3f6ea9bd35eb450837a3d80e77b517ea5bc56b4647f5502cd28de13675ee12f7"},
{file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"}, {file = "greenlet-2.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:7492e2b7bd7c9b9916388d9df23fa49d9b88ac0640db0a5b4ecc2b653bf451e3"},
{file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"}, {file = "greenlet-2.0.2-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:b864ba53912b6c3ab6bcb2beb19f19edd01a6bfcbdfe1f37ddd1778abfe75a30"},
{file = "greenlet-2.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1087300cf9700bbf455b1b97e24db18f2f77b55302a68272c56209d5587c12d1"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:ba2956617f1c42598a308a84c6cf021a90ff3862eddafd20c3333d50f0edb45b"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3a569657468b6f3fb60587e48356fe512c1754ca05a564f11366ac9e306526"},
{file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"}, {file = "greenlet-2.0.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8eab883b3b2a38cc1e050819ef06a7e6344d4a990d24d45bc6f2cf959045a45b"},
@ -216,6 +219,7 @@ files = [
{file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"}, {file = "greenlet-2.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b0ef99cdbe2b682b9ccbb964743a6aca37905fda5e0452e5ee239b1654d37f2a"},
{file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"}, {file = "greenlet-2.0.2-cp38-cp38-win32.whl", hash = "sha256:b80f600eddddce72320dbbc8e3784d16bd3fb7b517e82476d8da921f27d4b249"},
{file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"}, {file = "greenlet-2.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:4d2e11331fc0c02b6e84b0d28ece3a36e0548ee1a1ce9ddde03752d9b79bba40"},
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8512a0c38cfd4e66a858ddd1b17705587900dd760c6003998e9472b77b56d417"},
{file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"}, {file = "greenlet-2.0.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:88d9ab96491d38a5ab7c56dd7a3cc37d83336ecc564e4e8816dbed12e5aaefc8"},
{file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:561091a7be172ab497a3527602d467e2b3fbe75f9e783d8b8ce403fa414f71a6"},
{file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"}, {file = "greenlet-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:971ce5e14dc5e73715755d0ca2975ac88cfdaefcaab078a284fea6cfabf866df"},
@ -699,6 +703,24 @@ pluggy = ">=0.12,<2.0"
[package.extras] [package.extras]
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-asyncio"
version = "0.21.1"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-asyncio-0.21.1.tar.gz", hash = "sha256:40a7eae6dded22c7b604986855ea48400ab15b069ae38116e8c01238e9eeb64d"},
{file = "pytest_asyncio-0.21.1-py3-none-any.whl", hash = "sha256:8666c1c8ac02631d7c51ba282e0c69a8a452b211ffedf2599099845da5c5c37b"},
]
[package.dependencies]
pytest = ">=7.0.0"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"]
[[package]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.8.2" version = "2.8.2"
@ -739,6 +761,7 @@ files = [
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
@ -746,8 +769,15 @@ files = [
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
@ -764,6 +794,7 @@ files = [
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
@ -771,6 +802,7 @@ files = [
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
@ -849,7 +881,7 @@ files = [
] ]
[package.dependencies] [package.dependencies]
greenlet = {version = "!=0.4.17", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} greenlet = {version = "!=0.4.17", markers = "platform_machine == \"win32\" or platform_machine == \"WIN32\" or platform_machine == \"AMD64\" or platform_machine == \"amd64\" or platform_machine == \"x86_64\" or platform_machine == \"ppc64le\" or platform_machine == \"aarch64\""}
typing-extensions = ">=4.2.0" typing-extensions = ">=4.2.0"
[package.extras] [package.extras]
@ -942,4 +974,4 @@ anyio = ">=3.0.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = "^3.11"
content-hash = "28c7f9f4b78e7d602b076108d3138c854cd1a1bc22c447987a6aedbba6300dcd" content-hash = "2f47c374edfb8ee537dd4eeb7406bd443bdf0ed3bdd760e764a363ffd1b4650f"

View File

@ -18,6 +18,7 @@ pydantic-settings = "^2.0.3"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
pytest = "^7.4.2" pytest = "^7.4.2"
watchfiles = "^0.20.0" watchfiles = "^0.20.0"
pytest-asyncio = "^0.21.1"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]

11
settings_test.py Normal file
View File

@ -0,0 +1,11 @@
import logging
POSTGRES_HOST: str = "localhost"
POSTGRES_PORT: int = 5432
POSTGRES_TEST_USER = "addressbook"
POSTGRES_TEST_PASSWORD = "addressbook"
LOG_PATH = "/tmp/addressbook-log"
MIN_LOG_LEVEL: int = logging.INFO
DROP_TEMPLATE_DATABASE_BEFORE_TESTS: bool = False
FLYWAY_BINARY_PATH: str = "/usr/bin/flyway"

0
tests/__init__.py Normal file
View File

109
tests/conftest.py Normal file
View File

@ -0,0 +1,109 @@
import asyncio
import logging
from datetime import datetime
from typing import AsyncGenerator
import msgspec
import pytest_asyncio
import sqlalchemy
from _pytest.config import Config
from sqlalchemy import NullPool, event
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
from app.lib import settings
from app.lib.sqlalchemy_plugin import _default
from app.lib.test_extras.db_setup import TestingDatabaseSetup
# A Guide To Database Unit Testing with Pytest and SQLAlchemy
# https://coderpad.io/blog/development/a-guide-to-database-unit-testing-with-pytest-and-sqlalchemy/
db_connection_url = sqlalchemy.engine.URL.create(
drivername="postgresql+asyncpg",
username=settings.testing.DB_USER,
password=settings.testing.DB_PASSWORD,
host=settings.testing.DB_HOST,
port=settings.testing.DB_PORT,
database=settings.testing.DB_NAME,
)
engine = create_async_engine(
db_connection_url,
echo=settings.db.ECHO,
echo_pool=settings.db.ECHO_POOL,
json_serializer=msgspec.json.Encoder(enc_hook=_default),
max_overflow=settings.db.POOL_MAX_OVERFLOW,
pool_size=settings.db.POOL_SIZE,
pool_timeout=settings.db.POOL_TIMEOUT,
poolclass=NullPool if settings.db.POOL_DISABLE else None,
)
async_session_factory = async_sessionmaker(
engine, expire_on_commit=False, class_=AsyncSession
)
TestingAsyncSessionLocal = async_sessionmaker(
engine,
expire_on_commit=False,
autoflush=False,
autocommit=False,
class_=AsyncSession,
)
@pytest_asyncio.fixture(scope="function")
async def db_session() -> AsyncGenerator[AsyncSession, None]:
"""The expectation with async_sessions is that the
transactions be called on the connection object instead of the
session object.
Detailed explanation of async transactional tests
<https://github.com/sqlalchemy/sqlalchemy/issues/5811>
"""
async with engine.connect() as connection:
trans = await connection.begin()
async with TestingAsyncSessionLocal(bind=connection) as async_session:
nested = await connection.begin_nested()
@event.listens_for(async_session.sync_session, "after_transaction_end")
def end_savepoint(session, transaction):
nonlocal nested
if not nested.is_active:
nested = connection.sync_connection.begin_nested()
yield async_session
await trans.rollback()
await engine.dispose(close=True)
# @pytest.fixture(scope="session")
# def event_loop():
# """
# Creates an instance of the default event loop for the test session.
# """
# policy = asyncio.get_event_loop_policy()
# loop = policy.new_event_loop()
# yield loop
# loop.close()
pytest_plugins = (
# "app.lib.test_extras.db_plugins",
)
def pytest_configure(config: Config) -> None:
logging.info(f"Starting tests: {datetime.utcnow()}")
db_setup = TestingDatabaseSetup(options=settings.testing)
asyncio.run(db_setup.init_db())
print()
def pytest_unconfigure(config: Config) -> None:
logging.info(f"Ending tests: {datetime.utcnow()}")
db_setup = TestingDatabaseSetup(options=settings.testing)
asyncio.run(db_setup.tear_down_db())

41
tests/test_general.py Normal file
View File

@ -0,0 +1,41 @@
# A Guide To Database Unit Testing with Pytest and SQLAlchemy
# https://coderpad.io/blog/development/a-guide-to-database-unit-testing-with-pytest-and-sqlalchemy/
import pytest
from sqlalchemy import text, event
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from app.domain.city import City
from app.lib import settings
from app.lib.sqlalchemy_plugin import engine
import pytest
import pytest_asyncio
import sqlalchemy
from sqlalchemy.ext.asyncio import (
AsyncSession,
create_async_engine,
async_scoped_session,
AsyncConnection,
)
class TestGeneral:
@pytest.fixture(scope="function", autouse=True)
def setup_class(self, db_session):
self.db_session = db_session
# async def teardown_class(self):
# await self.session.rollback()
# await self.session.close()
@pytest.mark.asyncio
async def test_bla(self):
stmt = text("select * from cities")
result = await self.db_session.execute(stmt)
print("#"*100)
for c in result:
print(c)
print("#"*100)
assert True