Skip to content

Commit 5d34b04

Browse files
committed
Add test database configuration and schema creation for testing
1 parent f90513a commit 5d34b04

9 files changed

Lines changed: 96 additions & 17 deletions

File tree

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ POSTGRES_HOST=postgres
66
POSTGRES_PORT=5432
77
POSTGRES_DB=devdb
88
POSTGRES_USER=devdb
9+
POSTGRES_TEST_DB=testdb
10+
POSTGRES_TEST_USER=testdb
911
POSTGRES_PASSWORD=secret
1012

1113
# Redis

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ docker-create-db-migration: ## Create a new alembic database migration. Example
4545
# ====================================================================================
4646
.PHONY: docker-test
4747
docker-test: ## Run project tests
48-
docker compose -f compose.yml -f test-compose.yml run --rm api1 pytest tests --durations=0 -vv
48+
docker compose -f compose.yml run --rm api1 pytest tests --durations=0 -vv
4949

5050
.PHONY: docker-test-snapshot
5151
docker-test-snapshot: ## Run project tests and update snapshots

app/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ class Settings(BaseSettings):
3333
POSTGRES_PASSWORD: str
3434
POSTGRES_HOST: str
3535
POSTGRES_DB: str
36+
POSTGRES_TEST_USER: str
37+
POSTGRES_TEST_DB: str
3638

3739
@computed_field
3840
@property
@@ -80,6 +82,17 @@ def asyncpg_url(self) -> PostgresDsn:
8082
path=self.POSTGRES_DB,
8183
)
8284

85+
@computed_field
86+
@property
87+
def test_asyncpg_url(self) -> PostgresDsn:
88+
return MultiHostUrl.build(
89+
scheme="postgresql+asyncpg",
90+
username=self.POSTGRES_USER,
91+
password=self.POSTGRES_PASSWORD,
92+
host=self.POSTGRES_HOST,
93+
path=self.POSTGRES_TEST_DB,
94+
)
95+
8396
@computed_field
8497
@property
8598
def postgres_url(self) -> PostgresDsn:

app/database.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
echo=True,
1616
)
1717

18+
test_engine = create_async_engine(
19+
global_settings.test_asyncpg_url.unicode_string(),
20+
future=True,
21+
echo=True,
22+
)
23+
1824
# expire_on_commit=False will prevent attributes from being expired
1925
# after commit.
2026
AsyncSessionFactory = async_sessionmaker(
@@ -23,6 +29,12 @@
2329
expire_on_commit=False,
2430
)
2531

32+
TestAsyncSessionFactory = async_sessionmaker(
33+
test_engine,
34+
autoflush=False,
35+
expire_on_commit=False,
36+
)
37+
2638

2739
# Dependency
2840
async def get_db() -> AsyncGenerator:
@@ -38,3 +50,18 @@ async def get_db() -> AsyncGenerator:
3850
if not isinstance(ex, ResponseValidationError):
3951
await logger.aerror(f"Database-related error: {repr(ex)}")
4052
raise # Re-raise to be handled by appropriate handlers
53+
54+
55+
async def get_test_db() -> AsyncGenerator:
56+
async with TestAsyncSessionFactory() as session:
57+
try:
58+
yield session
59+
await session.commit()
60+
except SQLAlchemyError:
61+
# Re-raise SQLAlchemy errors to be handled by the global handler
62+
raise
63+
except Exception as ex:
64+
# Only log actual database-related issues, not response validation
65+
if not isinstance(ex, ResponseValidationError):
66+
await logger.aerror(f"Database-related error: {repr(ex)}")
67+
raise # Re-raise to be handled by appropriate handlers

db/Dockerfile

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# pull official base image
22
FROM postgres:17.6-alpine
33

4-
# run create.sql on init
5-
ADD create.sql /docker-entrypoint-initdb.d
6-
74
WORKDIR /home/gx/code
85

96
COPY shakespeare_chapter.sql /home/gx/code/shakespeare_chapter.sql

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ dev-dependencies = [
4040
"ipython>=9.5.0",
4141
"sqlacodegen<=3.1.1",
4242
"tryceratops>=2.4.1",
43-
"locust>=2.40.5"
44-
43+
"locust>=2.40.5",
44+
"sqlalchemy-utils>=0.41.1"
4545
]
4646

4747

test-compose.yml

Lines changed: 0 additions & 9 deletions
This file was deleted.

tests/conftest.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from collections.abc import AsyncGenerator
2+
from types import SimpleNamespace
23
from typing import Any
34

45
import pytest
56
from httpx import ASGITransport, AsyncClient
7+
from sqlalchemy import text
8+
from sqlalchemy.exc import ProgrammingError
69

7-
from app.database import engine
10+
from app.database import engine, test_engine, get_test_db, get_db
811
from app.main import app
912
from app.models.base import Base
1013
from app.redis import get_redis
@@ -19,15 +22,46 @@
1922
def anyio_backend(request):
2023
return request.param
2124

25+
def _create_db(conn) -> None:
26+
"""Create a database schema if it doesn't exist."""
27+
try:
28+
conn.execute(text("CREATE DATABASE testdb"))
29+
except ProgrammingError:
30+
# This might be raised by databases that don't support `IF NOT EXISTS`
31+
# and the schema already exists. You can choose to ignore it.
32+
pass
33+
34+
35+
def _create_db_schema(conn) -> None:
36+
"""Create a database schema if it doesn't exist."""
37+
try:
38+
conn.execute(text("CREATE SCHEMA happy_hog"))
39+
conn.execute(text("CREATE SCHEMA shakespeare"))
40+
except ProgrammingError:
41+
# This might be raised by databases that don't support `IF NOT EXISTS`
42+
# and the schema already exists. You can choose to ignore it.
43+
pass
44+
2245

2346
@pytest.fixture(scope="session")
2447
async def start_db():
25-
async with engine.begin() as conn:
48+
# The `engine` is configured for the default 'postgres' database.
49+
# We connect to it and create the test database.
50+
# A transaction block is not used, as CREATE DATABASE cannot run inside it.
51+
async with engine.connect() as conn:
52+
await conn.execute(text("COMMIT")) # Ensure we're not in a transaction
53+
await conn.run_sync(_create_db)
54+
55+
# Now, connect to the newly created `testdb` with `test_engine`
56+
async with test_engine.begin() as conn:
57+
await conn.execute(text("COMMIT")) # Ensure we're not in a transaction
58+
await conn.run_sync(_create_db_schema)
2659
await conn.run_sync(Base.metadata.drop_all)
2760
await conn.run_sync(Base.metadata.create_all)
2861
# for AsyncEngine created in function scope, close and
2962
# clean-up pooled connections
3063
await engine.dispose()
64+
await test_engine.dispose()
3165

3266

3367
@pytest.fixture(scope="session")
@@ -40,5 +74,6 @@ async def client(start_db) -> AsyncGenerator[AsyncClient, Any]: # noqa: ARG001
4074
headers={"Content-Type": "application/json"},
4175
transport=transport,
4276
) as test_client:
77+
app.dependency_overrides[get_db] = get_test_db
4378
app.redis = await get_redis()
4479
yield test_client

uv.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)