Skip to content

Commit a386968

Browse files
feat(tests): update E2E test database configuration
- Change E2E tests to use a separate 'test' database instead of 'test_e2e' schema - Update documentation and comments to reflect the new database setup
1 parent ef5a87e commit a386968

File tree

7 files changed

+44
-41
lines changed

7 files changed

+44
-41
lines changed

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ DATABASE_URL=postgresql+asyncpg://pyplots:PASSWORD@34.7.157.136:5432/pyplots
2222
# For Cloud Run (uses Unix socket via Cloud SQL connector):
2323
# DATABASE_URL=postgresql+asyncpg://pyplots:PASSWORD@/pyplots?host=/cloudsql/pyplots:europe-west4:pyplots-db
2424

25+
# For E2E tests (optional - defaults to 'test' database on same instance):
26+
# TEST_DATABASE_URL=postgresql+asyncpg://pyplots:PASSWORD@34.7.157.136:5432/test
27+
2528
# ============================================================================
2629
# API Configuration
2730
# ============================================================================

.github/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ Examples: `scatter-basic`, `scatter-color-mapped`, `bar-grouped-horizontal`, `he
169169
- **Test Types**:
170170
- **Unit tests** (`tests/unit/`): Fast, isolated, mocked dependencies
171171
- **Integration tests** (`tests/integration/`): SQLite in-memory for API tests
172-
- **E2E tests** (`tests/e2e/`): Real PostgreSQL with isolated `test_e2e` schema (skipped if DATABASE_URL not set)
172+
- **E2E tests** (`tests/e2e/`): Real PostgreSQL with separate `test` database (skipped if DATABASE_URL not set)
173173
- **Markers**: Use `@pytest.mark.unit`, `@pytest.mark.integration`, `@pytest.mark.e2e`
174174

175175
### Plot Implementation Style (KISS)

.github/workflows/ci-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ jobs:
102102
# Tests include:
103103
# - Unit tests: Fast, mocked dependencies
104104
# - Integration tests: SQLite in-memory for repository layer
105-
# - E2E tests: Real PostgreSQL with isolated test_e2e schema
105+
# - E2E tests: Real PostgreSQL with separate 'test' database
106106

107107
- name: Upload coverage to GitHub Artifacts
108108
if: steps.check.outputs.should_test == 'true' && always()

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,11 +154,11 @@ uv run pytest tests/unit/api/test_routers.py::test_get_specs
154154
**Test Infrastructure**:
155155
- **Unit tests** (`tests/unit/`): Fast, mocked dependencies
156156
- **Integration tests** (`tests/integration/`): SQLite in-memory for API tests
157-
- **E2E tests** (`tests/e2e/`): Real PostgreSQL with isolated `test_e2e` schema
157+
- **E2E tests** (`tests/e2e/`): Real PostgreSQL with separate `test` database
158158

159159
**Database for Tests**:
160160
- **Unit/Integration**: SQLite in-memory (via custom types in `core/database/types.py`)
161-
- **E2E**: PostgreSQL with `test_e2e` schema (auto-created, auto-dropped)
161+
- **E2E**: PostgreSQL with separate `test` database (tables auto-created, auto-dropped)
162162
- E2E tests are skipped if `DATABASE_URL` is not set
163163

164164
### Code Quality

tests/e2e/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
"""E2E tests with real PostgreSQL database (isolated test_e2e schema)."""
1+
"""E2E tests with real PostgreSQL database (separate 'test' database)."""

tests/e2e/conftest.py

Lines changed: 35 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
"""
22
E2E test fixtures with real PostgreSQL database.
33
4-
Uses a separate 'test_e2e' schema to isolate test data from production.
4+
Uses a separate 'test' database to isolate test data from production.
55
Tests are skipped if DATABASE_URL is not set or database is unreachable.
66
77
Connection modes:
8-
- Local: Direct DATABASE_URL from .env
8+
- Local: Direct DATABASE_URL from .env (auto-derives test database)
99
- CI: DATABASE_URL via Cloud SQL Proxy (localhost:5432 -> Cloud SQL)
10+
- Explicit: TEST_DATABASE_URL for custom test database
1011
1112
Note: These tests must NOT be run with pytest-xdist parallelization
12-
as multiple workers would conflict on the shared test_e2e schema.
13+
as multiple workers would conflict on the shared test database.
1314
"""
1415

1516
import asyncio
1617
import os
1718

1819
import pytest
1920
import pytest_asyncio
20-
from sqlalchemy import text
2121
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
2222

2323
from core.database.models import Base
2424

2525

26-
TEST_SCHEMA = "test_e2e"
2726
CONNECTION_TIMEOUT = 10 # seconds - skip tests if DB unreachable
2827

2928
# Test data constants
@@ -32,14 +31,21 @@
3231

3332

3433
def _get_database_url():
35-
"""Get DATABASE_URL from environment, loading .env if needed."""
34+
"""Get test database URL, defaulting to 'test' database on same instance."""
3635
from dotenv import load_dotenv
3736

3837
load_dotenv()
3938

40-
database_url = os.environ.get("DATABASE_URL")
39+
# Prefer explicit TEST_DATABASE_URL
40+
database_url = os.environ.get("TEST_DATABASE_URL")
41+
4142
if not database_url:
42-
return None
43+
# Derive from DATABASE_URL by replacing database name with 'test'
44+
prod_url = os.environ.get("DATABASE_URL")
45+
if not prod_url:
46+
return None
47+
# Replace database name at end of URL (e.g., /pyplots -> /test)
48+
database_url = prod_url.rsplit("/", 1)[0] + "/test"
4349

4450
# Ensure async driver
4551
if database_url.startswith("postgresql://"):
@@ -53,53 +59,47 @@ def _get_database_url():
5359
@pytest_asyncio.fixture(scope="function")
5460
async def pg_engine():
5561
"""
56-
Create PostgreSQL engine and setup test schema.
62+
Create PostgreSQL engine for test database.
5763
58-
Creates a separate 'test_e2e' schema to isolate tests from production data.
59-
The schema is dropped and recreated for each test.
64+
Uses a separate 'test' database to isolate tests from production.
65+
Tables are dropped and recreated for each test.
6066
Skips tests if database is unreachable.
6167
"""
6268
database_url = _get_database_url()
6369
if not database_url:
6470
pytest.skip("DATABASE_URL not set - skipping PostgreSQL E2E tests")
6571

66-
# Create temporary engine for schema setup
67-
temp_engine = create_async_engine(database_url, echo=False, connect_args={"timeout": CONNECTION_TIMEOUT})
72+
engine = create_async_engine(
73+
database_url,
74+
echo=False,
75+
connect_args={"timeout": CONNECTION_TIMEOUT},
76+
)
77+
6878
try:
6979
async with asyncio.timeout(CONNECTION_TIMEOUT + 2):
70-
async with temp_engine.begin() as conn:
71-
await conn.execute(text(f"DROP SCHEMA IF EXISTS {TEST_SCHEMA} CASCADE"))
72-
await conn.execute(text(f"CREATE SCHEMA {TEST_SCHEMA}"))
73-
await temp_engine.dispose()
80+
async with engine.begin() as conn:
81+
# Drop all tables for clean state
82+
await conn.run_sync(Base.metadata.drop_all)
83+
# Create fresh tables
84+
await conn.run_sync(Base.metadata.create_all)
7485
except (TimeoutError, asyncio.TimeoutError, OSError) as e:
75-
await temp_engine.dispose()
86+
await engine.dispose()
7687
pytest.skip(f"Database unreachable (timeout) - skipping E2E tests: {e}")
7788
except Exception as e:
78-
await temp_engine.dispose()
89+
await engine.dispose()
7990
pytest.skip(f"Database connection failed - skipping E2E tests: {e}")
8091

81-
# Create main engine with search_path set at connection level
82-
engine = create_async_engine(
83-
database_url,
84-
echo=False,
85-
connect_args={"server_settings": {"search_path": TEST_SCHEMA}, "timeout": CONNECTION_TIMEOUT},
86-
)
87-
88-
# Create tables in test schema
89-
async with engine.begin() as conn:
90-
await conn.run_sync(Base.metadata.create_all)
91-
9292
yield engine
9393

94-
# Cleanup: Drop entire test schema
94+
# Cleanup: Drop all tables
9595
async with engine.begin() as conn:
96-
await conn.execute(text(f"DROP SCHEMA IF EXISTS {TEST_SCHEMA} CASCADE"))
96+
await conn.run_sync(Base.metadata.drop_all)
9797
await engine.dispose()
9898

9999

100100
@pytest_asyncio.fixture
101101
async def pg_session(pg_engine):
102-
"""Create session with test schema (search_path set at engine level)."""
102+
"""Create session for test database."""
103103
async_session = async_sessionmaker(pg_engine, class_=AsyncSession, expire_on_commit=False)
104104
async with async_session() as session:
105105
yield session
@@ -109,10 +109,10 @@ async def pg_session(pg_engine):
109109
@pytest_asyncio.fixture
110110
async def pg_db_with_data(pg_session):
111111
"""
112-
Seed test schema with sample data.
112+
Seed test database with sample data.
113113
114114
Creates the same test data as tests/conftest.py:test_db_with_data
115-
but in the PostgreSQL test schema.
115+
but in the PostgreSQL test database.
116116
"""
117117
from core.database.models import Impl, Library, Spec
118118

tests/e2e/test_api_postgres.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Tests full API stack against a real PostgreSQL database.
55
No mocking - uses actual database connection via DATABASE_URL.
6-
Tests run in isolated 'test_e2e' schema to protect production data.
6+
Tests run in separate 'test' database to protect production data.
77
88
These tests are skipped if DATABASE_URL is not set, allowing
99
local development without PostgreSQL.

0 commit comments

Comments
 (0)