From ecb5309879cf8c669944f6326e103971f5ac0e84 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Tue, 10 Feb 2026 21:42:06 +0100 Subject: [PATCH] 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 --- Makefile | 4 +- app/modules/billing/tests/__init__.py | 0 app/modules/billing/tests/conftest.py | 3 + app/modules/billing/tests/unit/__init__.py | 0 app/modules/catalog/tests/__init__.py | 0 app/modules/catalog/tests/conftest.py | 3 + app/modules/catalog/tests/unit/__init__.py | 0 app/modules/customers/tests/__init__.py | 0 app/modules/customers/tests/conftest.py | 3 + app/modules/customers/tests/unit/__init__.py | 0 app/modules/inventory/tests/__init__.py | 0 app/modules/inventory/tests/conftest.py | 3 + app/modules/inventory/tests/unit/__init__.py | 0 app/modules/loyalty/tests/__init__.py | 0 app/modules/loyalty/tests/conftest.py | 3 + .../loyalty/tests/integration/__init__.py | 0 app/modules/marketplace/tests/__init__.py | 0 app/modules/marketplace/tests/conftest.py | 3 + .../marketplace/tests/unit/__init__.py | 0 app/modules/messaging/tests/__init__.py | 0 app/modules/messaging/tests/conftest.py | 3 + app/modules/messaging/tests/unit/__init__.py | 0 app/modules/orders/tests/__init__.py | 0 app/modules/orders/tests/conftest.py | 3 + app/modules/orders/tests/unit/__init__.py | 0 app/modules/tenancy/tests/__init__.py | 0 app/modules/tenancy/tests/conftest.py | 3 + .../tenancy/tests/integration/__init__.py | 0 app/modules/tenancy/tests/unit/__init__.py | 0 conftest.py | 180 +++++++++++++++++ pyproject.toml | 14 +- tests/conftest.py | 184 +----------------- tests/e2e/__init__.py | 0 tests/e2e/conftest.py | 2 + 34 files changed, 228 insertions(+), 183 deletions(-) create mode 100644 app/modules/billing/tests/__init__.py create mode 100644 app/modules/billing/tests/conftest.py create mode 100644 app/modules/billing/tests/unit/__init__.py create mode 100644 app/modules/catalog/tests/__init__.py create mode 100644 app/modules/catalog/tests/conftest.py create mode 100644 app/modules/catalog/tests/unit/__init__.py create mode 100644 app/modules/customers/tests/__init__.py create mode 100644 app/modules/customers/tests/conftest.py create mode 100644 app/modules/customers/tests/unit/__init__.py create mode 100644 app/modules/inventory/tests/__init__.py create mode 100644 app/modules/inventory/tests/conftest.py create mode 100644 app/modules/inventory/tests/unit/__init__.py create mode 100644 app/modules/loyalty/tests/__init__.py create mode 100644 app/modules/loyalty/tests/conftest.py create mode 100644 app/modules/loyalty/tests/integration/__init__.py create mode 100644 app/modules/marketplace/tests/__init__.py create mode 100644 app/modules/marketplace/tests/conftest.py create mode 100644 app/modules/marketplace/tests/unit/__init__.py create mode 100644 app/modules/messaging/tests/__init__.py create mode 100644 app/modules/messaging/tests/conftest.py create mode 100644 app/modules/messaging/tests/unit/__init__.py create mode 100644 app/modules/orders/tests/__init__.py create mode 100644 app/modules/orders/tests/conftest.py create mode 100644 app/modules/orders/tests/unit/__init__.py create mode 100644 app/modules/tenancy/tests/__init__.py create mode 100644 app/modules/tenancy/tests/conftest.py create mode 100644 app/modules/tenancy/tests/integration/__init__.py create mode 100644 app/modules/tenancy/tests/unit/__init__.py create mode 100644 conftest.py create mode 100644 tests/e2e/__init__.py create mode 100644 tests/e2e/conftest.py diff --git a/Makefile b/Makefile index 2872959e..1a86ef57 100644 --- a/Makefile +++ b/Makefile @@ -250,8 +250,8 @@ ifdef frontend endif endif -# All testpaths -TEST_PATHS := tests/ +# All testpaths (central + module 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: @docker compose -f docker-compose.test.yml up -d 2>/dev/null || true diff --git a/app/modules/billing/tests/__init__.py b/app/modules/billing/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/billing/tests/conftest.py b/app/modules/billing/tests/conftest.py new file mode 100644 index 00000000..ac166ce6 --- /dev/null +++ b/app/modules/billing/tests/conftest.py @@ -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. diff --git a/app/modules/billing/tests/unit/__init__.py b/app/modules/billing/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/catalog/tests/__init__.py b/app/modules/catalog/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/catalog/tests/conftest.py b/app/modules/catalog/tests/conftest.py new file mode 100644 index 00000000..583afe93 --- /dev/null +++ b/app/modules/catalog/tests/conftest.py @@ -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. diff --git a/app/modules/catalog/tests/unit/__init__.py b/app/modules/catalog/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/customers/tests/__init__.py b/app/modules/customers/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/customers/tests/conftest.py b/app/modules/customers/tests/conftest.py new file mode 100644 index 00000000..48cfb571 --- /dev/null +++ b/app/modules/customers/tests/conftest.py @@ -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. diff --git a/app/modules/customers/tests/unit/__init__.py b/app/modules/customers/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/inventory/tests/__init__.py b/app/modules/inventory/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/inventory/tests/conftest.py b/app/modules/inventory/tests/conftest.py new file mode 100644 index 00000000..6b5369bf --- /dev/null +++ b/app/modules/inventory/tests/conftest.py @@ -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. diff --git a/app/modules/inventory/tests/unit/__init__.py b/app/modules/inventory/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/loyalty/tests/__init__.py b/app/modules/loyalty/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/loyalty/tests/conftest.py b/app/modules/loyalty/tests/conftest.py new file mode 100644 index 00000000..64c6f913 --- /dev/null +++ b/app/modules/loyalty/tests/conftest.py @@ -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. diff --git a/app/modules/loyalty/tests/integration/__init__.py b/app/modules/loyalty/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/marketplace/tests/__init__.py b/app/modules/marketplace/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/marketplace/tests/conftest.py b/app/modules/marketplace/tests/conftest.py new file mode 100644 index 00000000..a19f4a8c --- /dev/null +++ b/app/modules/marketplace/tests/conftest.py @@ -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. diff --git a/app/modules/marketplace/tests/unit/__init__.py b/app/modules/marketplace/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/messaging/tests/__init__.py b/app/modules/messaging/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/messaging/tests/conftest.py b/app/modules/messaging/tests/conftest.py new file mode 100644 index 00000000..63f5f2db --- /dev/null +++ b/app/modules/messaging/tests/conftest.py @@ -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. diff --git a/app/modules/messaging/tests/unit/__init__.py b/app/modules/messaging/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/orders/tests/__init__.py b/app/modules/orders/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/orders/tests/conftest.py b/app/modules/orders/tests/conftest.py new file mode 100644 index 00000000..23828108 --- /dev/null +++ b/app/modules/orders/tests/conftest.py @@ -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. diff --git a/app/modules/orders/tests/unit/__init__.py b/app/modules/orders/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/tenancy/tests/__init__.py b/app/modules/tenancy/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/tenancy/tests/conftest.py b/app/modules/tenancy/tests/conftest.py new file mode 100644 index 00000000..1735df7a --- /dev/null +++ b/app/modules/tenancy/tests/conftest.py @@ -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. diff --git a/app/modules/tenancy/tests/integration/__init__.py b/app/modules/tenancy/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/app/modules/tenancy/tests/unit/__init__.py b/app/modules/tenancy/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..91e21cee --- /dev/null +++ b/conftest.py @@ -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", +] diff --git a/pyproject.toml b/pyproject.toml index bae6fbad..759993f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,6 +66,7 @@ unfixable = [] "models/database/base.py" = ["F401"] # Ignore specific rules in test files "tests/**/*.py" = ["S101", "PLR2004"] +"app/modules/*/tests/**/*.py" = ["S101", "PLR2004"] # Alembic migrations can have longer lines and specific patterns "alembic/versions/*.py" = ["E501", "F401"] @@ -113,7 +114,18 @@ ignore_errors = true # ============================================================================= [tool.pytest.ini_options] 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_classes = ["Test*"] python_functions = ["test_*"] diff --git a/tests/conftest.py b/tests/conftest.py index 405578d0..077a88c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,180 +1,4 @@ -# tests/conftest.py - PostgreSQL test configuration -""" -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", -] +# tests/conftest.py +# Core fixtures (engine, db, client, cleanup, pytest_plugins) are defined in +# the project-root conftest.py so that both tests/ and app/modules/*/tests/ +# inherit them automatically. diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py new file mode 100644 index 00000000..eb4601b8 --- /dev/null +++ b/tests/e2e/conftest.py @@ -0,0 +1,2 @@ +# tests/e2e/conftest.py +"""E2E test specific fixtures for multi-module user journeys."""