refactor(tests): reorganize tests per module with shared root conftest

Move 42 single-module test files into app/modules/*/tests/ directories
while keeping 40 cross-module and infrastructure tests central in tests/.
Hub fixtures (engine, db, client, cleanup) moved to root conftest.py so
both tests/ and module tests inherit them. Update pyproject.toml testpaths
and Makefile TEST_PATHS to discover all test locations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 21:42:06 +01:00
parent 1da03e41f9
commit ecb5309879
34 changed files with 228 additions and 183 deletions

View File

@@ -250,8 +250,8 @@ ifdef frontend
endif endif
endif endif
# All testpaths # All testpaths (central + module tests)
TEST_PATHS := tests/ TEST_PATHS := tests/ app/modules/tenancy/tests/ app/modules/catalog/tests/ app/modules/billing/tests/ app/modules/messaging/tests/ app/modules/orders/tests/ app/modules/customers/tests/ app/modules/marketplace/tests/ app/modules/inventory/tests/ app/modules/loyalty/tests/
test: test:
@docker compose -f docker-compose.test.yml up -d 2>/dev/null || true @docker compose -f docker-compose.test.yml up -d 2>/dev/null || true

View File

View File

@@ -0,0 +1,3 @@
# app/modules/billing/tests/conftest.py
# Module-specific fixtures for billing tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

View File

View File

@@ -0,0 +1,3 @@
# app/modules/catalog/tests/conftest.py
# Module-specific fixtures for catalog tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

View File

View File

@@ -0,0 +1,3 @@
# app/modules/customers/tests/conftest.py
# Module-specific fixtures for customers tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

View File

View File

@@ -0,0 +1,3 @@
# app/modules/inventory/tests/conftest.py
# Module-specific fixtures for inventory tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

View File

View File

@@ -0,0 +1,3 @@
# app/modules/loyalty/tests/conftest.py
# Module-specific fixtures for loyalty tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

View File

@@ -0,0 +1,3 @@
# app/modules/marketplace/tests/conftest.py
# Module-specific fixtures for marketplace tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

View File

View File

@@ -0,0 +1,3 @@
# app/modules/messaging/tests/conftest.py
# Module-specific fixtures for messaging tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

View File

View File

@@ -0,0 +1,3 @@
# app/modules/orders/tests/conftest.py
# Module-specific fixtures for orders tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

View File

View File

@@ -0,0 +1,3 @@
# app/modules/tenancy/tests/conftest.py
# Module-specific fixtures for tenancy tests.
# Core fixtures (db, client, etc.) are inherited from the root conftest.py.

180
conftest.py Normal file
View File

@@ -0,0 +1,180 @@
# conftest.py - PostgreSQL test configuration (project root)
"""
Core pytest configuration and fixtures.
This project uses PostgreSQL for testing. Start the test database with:
make test-db-up
IMPORTANT - Fixture Best Practices:
===================================
1. DO NOT use db.expunge() on fixtures - it detaches objects from the session
and breaks lazy loading of relationships (e.g., product.marketplace_product).
2. Test isolation is achieved through TRUNCATE CASCADE after each test,
which is much faster than dropping/recreating tables.
3. If you need to ensure an object has fresh data, use db.refresh(obj) instead
of expunge/re-query patterns.
4. The session uses expire_on_commit=False to prevent objects from being expired
after commits, which helps with relationship access across operations.
See docs/testing/testing-guide.md for comprehensive testing documentation.
"""
import os
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.core.database import Base, get_db
from main import app
# PostgreSQL test database URL
# Use environment variable or default to local Docker test database
TEST_DATABASE_URL = os.getenv(
"TEST_DATABASE_URL",
"postgresql://test_user:test_password@localhost:5433/wizamart_test"
)
@pytest.fixture(scope="session")
def engine():
"""Create test database engine.
Verifies PostgreSQL connection on startup and provides helpful
error message if the test database is not running.
"""
engine = create_engine(
TEST_DATABASE_URL,
pool_pre_ping=True,
echo=False, # Set to True for SQL debugging
)
# Verify connection on startup
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
except Exception as e:
pytest.exit(
f"\n\nCannot connect to test database at {TEST_DATABASE_URL}\n"
f"Error: {e}\n\n"
"Start the test database with:\n"
" make test-db-up\n\n"
"Or manually:\n"
" docker-compose -f docker-compose.test.yml up -d\n"
)
return engine
@pytest.fixture(scope="session")
def testing_session_local(engine):
"""
Create session factory for tests.
Uses expire_on_commit=False to prevent objects from being expired after
commits. This allows fixtures to remain usable after database operations
without needing to refresh or re-query them.
"""
return sessionmaker(
autocommit=False,
autoflush=False,
bind=engine,
expire_on_commit=False, # Prevents lazy-load issues after commits
)
@pytest.fixture(scope="session", autouse=True)
def setup_database(engine):
"""Create all tables once at the start of the test session."""
with engine.connect() as conn:
# Terminate any other connections to avoid deadlocks
conn.execute(text("""
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = current_database() AND pid <> pg_backend_pid()
"""))
conn.execute(text("DROP SCHEMA public CASCADE"))
conn.execute(text("CREATE SCHEMA public"))
conn.commit()
Base.metadata.create_all(bind=engine)
yield
@pytest.fixture(scope="function")
def db(engine, testing_session_local):
"""
Create a database session for each test function.
Provides test isolation by:
- Using a fresh session for each test
- Truncating all tables after each test (fast cleanup)
Note: Fixtures should NOT use db.expunge() as this detaches objects
from the session and breaks lazy loading. The TRUNCATE provides
sufficient isolation between tests.
"""
db_session = testing_session_local()
try:
yield db_session
finally:
db_session.close()
# Fast cleanup with TRUNCATE CASCADE
with engine.connect() as conn:
# Disable FK checks temporarily for fast truncation
conn.execute(text("SET session_replication_role = 'replica'"))
for table in reversed(Base.metadata.sorted_tables):
conn.execute(text(f'TRUNCATE TABLE "{table.name}" CASCADE'))
conn.execute(text("SET session_replication_role = 'origin'"))
conn.commit()
@pytest.fixture(scope="function")
def client(db):
"""Create a test client with database dependency override"""
# Override the dependency to use our test database
def override_get_db():
try:
yield db
finally:
pass # Don't close here, the db fixture handles it
app.dependency_overrides[get_db] = override_get_db
try:
client = TestClient(app)
yield client
finally:
# Clean up the dependency override
if get_db in app.dependency_overrides:
del app.dependency_overrides[get_db]
# Cleanup fixture to ensure clean state
@pytest.fixture(autouse=True)
def cleanup():
"""Automatically clean up after each test"""
yield
# Clear any remaining dependency overrides
app.dependency_overrides.clear()
# Import fixtures from fixture modules
pytest_plugins = [
"tests.fixtures.admin_platform_fixtures",
"tests.fixtures.auth_fixtures",
"tests.fixtures.loyalty_fixtures",
"tests.fixtures.marketplace_product_fixtures",
"tests.fixtures.store_fixtures",
"tests.fixtures.customer_fixtures",
"tests.fixtures.marketplace_import_job_fixtures",
"tests.fixtures.message_fixtures",
"tests.fixtures.testing_fixtures",
"tests.fixtures.content_page_fixtures",
"tests.fixtures.merchant_domain_fixtures",
]

View File

@@ -66,6 +66,7 @@ unfixable = []
"models/database/base.py" = ["F401"] "models/database/base.py" = ["F401"]
# Ignore specific rules in test files # Ignore specific rules in test files
"tests/**/*.py" = ["S101", "PLR2004"] "tests/**/*.py" = ["S101", "PLR2004"]
"app/modules/*/tests/**/*.py" = ["S101", "PLR2004"]
# Alembic migrations can have longer lines and specific patterns # Alembic migrations can have longer lines and specific patterns
"alembic/versions/*.py" = ["E501", "F401"] "alembic/versions/*.py" = ["E501", "F401"]
@@ -113,7 +114,18 @@ ignore_errors = true
# ============================================================================= # =============================================================================
[tool.pytest.ini_options] [tool.pytest.ini_options]
minversion = "7.0" minversion = "7.0"
testpaths = ["tests"] testpaths = [
"tests",
"app/modules/tenancy/tests",
"app/modules/catalog/tests",
"app/modules/billing/tests",
"app/modules/messaging/tests",
"app/modules/orders/tests",
"app/modules/customers/tests",
"app/modules/marketplace/tests",
"app/modules/inventory/tests",
"app/modules/loyalty/tests",
]
python_files = ["test_*.py", "*_test.py"] python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"] python_classes = ["Test*"]
python_functions = ["test_*"] python_functions = ["test_*"]

View File

@@ -1,180 +1,4 @@
# tests/conftest.py - PostgreSQL test configuration # tests/conftest.py
""" # Core fixtures (engine, db, client, cleanup, pytest_plugins) are defined in
Core pytest configuration and fixtures. # the project-root conftest.py so that both tests/ and app/modules/*/tests/
# inherit them automatically.
This project uses PostgreSQL for testing. Start the test database with:
make test-db-up
IMPORTANT - Fixture Best Practices:
===================================
1. DO NOT use db.expunge() on fixtures - it detaches objects from the session
and breaks lazy loading of relationships (e.g., product.marketplace_product).
2. Test isolation is achieved through TRUNCATE CASCADE after each test,
which is much faster than dropping/recreating tables.
3. If you need to ensure an object has fresh data, use db.refresh(obj) instead
of expunge/re-query patterns.
4. The session uses expire_on_commit=False to prevent objects from being expired
after commits, which helps with relationship access across operations.
See docs/testing/testing-guide.md for comprehensive testing documentation.
"""
import os
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
from app.core.database import Base, get_db
from main import app
# PostgreSQL test database URL
# Use environment variable or default to local Docker test database
TEST_DATABASE_URL = os.getenv(
"TEST_DATABASE_URL",
"postgresql://test_user:test_password@localhost:5433/wizamart_test"
)
@pytest.fixture(scope="session")
def engine():
"""Create test database engine.
Verifies PostgreSQL connection on startup and provides helpful
error message if the test database is not running.
"""
engine = create_engine(
TEST_DATABASE_URL,
pool_pre_ping=True,
echo=False, # Set to True for SQL debugging
)
# Verify connection on startup
try:
with engine.connect() as conn:
conn.execute(text("SELECT 1"))
except Exception as e:
pytest.exit(
f"\n\nCannot connect to test database at {TEST_DATABASE_URL}\n"
f"Error: {e}\n\n"
"Start the test database with:\n"
" make test-db-up\n\n"
"Or manually:\n"
" docker-compose -f docker-compose.test.yml up -d\n"
)
return engine
@pytest.fixture(scope="session")
def testing_session_local(engine):
"""
Create session factory for tests.
Uses expire_on_commit=False to prevent objects from being expired after
commits. This allows fixtures to remain usable after database operations
without needing to refresh or re-query them.
"""
return sessionmaker(
autocommit=False,
autoflush=False,
bind=engine,
expire_on_commit=False, # Prevents lazy-load issues after commits
)
@pytest.fixture(scope="session", autouse=True)
def setup_database(engine):
"""Create all tables once at the start of the test session."""
with engine.connect() as conn:
# Terminate any other connections to avoid deadlocks
conn.execute(text("""
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = current_database() AND pid <> pg_backend_pid()
"""))
conn.execute(text("DROP SCHEMA public CASCADE"))
conn.execute(text("CREATE SCHEMA public"))
conn.commit()
Base.metadata.create_all(bind=engine)
yield
@pytest.fixture(scope="function")
def db(engine, testing_session_local):
"""
Create a database session for each test function.
Provides test isolation by:
- Using a fresh session for each test
- Truncating all tables after each test (fast cleanup)
Note: Fixtures should NOT use db.expunge() as this detaches objects
from the session and breaks lazy loading. The TRUNCATE provides
sufficient isolation between tests.
"""
db_session = testing_session_local()
try:
yield db_session
finally:
db_session.close()
# Fast cleanup with TRUNCATE CASCADE
with engine.connect() as conn:
# Disable FK checks temporarily for fast truncation
conn.execute(text("SET session_replication_role = 'replica'"))
for table in reversed(Base.metadata.sorted_tables):
conn.execute(text(f'TRUNCATE TABLE "{table.name}" CASCADE'))
conn.execute(text("SET session_replication_role = 'origin'"))
conn.commit()
@pytest.fixture(scope="function")
def client(db):
"""Create a test client with database dependency override"""
# Override the dependency to use our test database
def override_get_db():
try:
yield db
finally:
pass # Don't close here, the db fixture handles it
app.dependency_overrides[get_db] = override_get_db
try:
client = TestClient(app)
yield client
finally:
# Clean up the dependency override
if get_db in app.dependency_overrides:
del app.dependency_overrides[get_db]
# Cleanup fixture to ensure clean state
@pytest.fixture(autouse=True)
def cleanup():
"""Automatically clean up after each test"""
yield
# Clear any remaining dependency overrides
app.dependency_overrides.clear()
# Import fixtures from fixture modules
pytest_plugins = [
"tests.fixtures.admin_platform_fixtures",
"tests.fixtures.auth_fixtures",
"tests.fixtures.loyalty_fixtures",
"tests.fixtures.marketplace_product_fixtures",
"tests.fixtures.store_fixtures",
"tests.fixtures.customer_fixtures",
"tests.fixtures.marketplace_import_job_fixtures",
"tests.fixtures.message_fixtures",
"tests.fixtures.testing_fixtures",
"tests.fixtures.content_page_fixtures",
"tests.fixtures.merchant_domain_fixtures",
]

0
tests/e2e/__init__.py Normal file
View File

2
tests/e2e/conftest.py Normal file
View File

@@ -0,0 +1,2 @@
# tests/e2e/conftest.py
"""E2E test specific fixtures for multi-module user journeys."""