From cf0492f3af6b6bbf470f987ef7c7d2afe180b8fd Mon Sep 17 00:00:00 2001 From: Nitin Ahuja Date: Tue, 10 Mar 2026 10:58:10 -0700 Subject: [PATCH] refactor: Extract reusable framework from tests - Create framework/ package with assertions and fixtures modules - Move tests/common/assertions.py to framework/assertions.py - Extract fixture utilities from conftest.py to framework/fixtures.py - Update all test files to import from framework - Update conftest.py to use framework.fixtures - Remove tests/common/ directory Signed-off-by: Nitin Ahuja --- conftest.py | 55 ++-------- framework/__init__.py | 11 ++ {tests/common => framework}/assertions.py | 2 +- framework/fixtures.py | 123 ++++++++++++++++++++++ tests/__init__.py | 2 +- tests/common/__init__.py | 1 - tests/find/test_basic_queries.py | 2 +- tests/find/test_projections.py | 2 +- tests/insert/test_insert_operations.py | 2 +- 9 files changed, 147 insertions(+), 53 deletions(-) create mode 100644 framework/__init__.py rename {tests/common => framework}/assertions.py (99%) create mode 100644 framework/fixtures.py delete mode 100644 tests/common/__init__.py diff --git a/conftest.py b/conftest.py index dc5487f..a978a8e 100644 --- a/conftest.py +++ b/conftest.py @@ -7,10 +7,8 @@ - Test isolation """ - -import hashlib import pytest -from pymongo import MongoClient +from framework import fixtures def pytest_addoption(parser): @@ -70,18 +68,7 @@ def engine_client(request): connection_string = request.config.connection_string engine_name = request.config.engine_name - client = MongoClient(connection_string) - - # Verify connection - try: - client.admin.command("ping") - except Exception as e: - # Close the client before raising - client.close() - # Raise ConnectionError so analyzer categorizes as INFRA_ERROR - raise ConnectionError( - f"Cannot connect to {engine_name} at {connection_string}: {e}" - ) from e + client = fixtures.create_engine_client(connection_string, engine_name) yield client @@ -106,29 +93,16 @@ def database_client(engine_client, request, worker_id): Yields: Database: MongoDB database object """ - # Get full test identifier (includes file path and test name) + # Generate unique database name using framework utility full_test_id = request.node.nodeid - - # Create a short hash for uniqueness (first 8 chars of SHA256) - name_hash = hashlib.sha256(full_test_id.encode()).hexdigest()[:8] - - # Get abbreviated test name for readability (sanitize and truncate) - test_name = request.node.name.replace("[", "_").replace("]", "_") - abbreviated = test_name[:20] - - # Combine: test_{worker}_{hash}_{abbreviated} - # Example: test_gw0_a1b2c3d4_find_all_documents - db_name = f"test_{worker_id}_{name_hash}_{abbreviated}"[:63] # MongoDB limit + db_name = fixtures.generate_database_name(full_test_id, worker_id) db = engine_client[db_name] yield db # Cleanup: drop test database - try: - engine_client.drop_database(db_name) - except Exception: - pass # Best effort cleanup + fixtures.cleanup_database(engine_client, db_name) @pytest.fixture(scope="function") @@ -147,26 +121,13 @@ def collection(database_client, request, worker_id): Yields: Collection: Empty MongoDB collection object """ - # Get full test identifier + # Generate unique collection name using framework utility full_test_id = request.node.nodeid - - # Create a short hash for uniqueness (first 8 chars of SHA256) - name_hash = hashlib.sha256(full_test_id.encode()).hexdigest()[:8] - - # Get abbreviated test name for readability (sanitize and truncate) - test_name = request.node.name.replace("[", "_").replace("]", "_") - abbreviated = test_name[:25] - - # Combine: coll_{worker}_{hash}_{abbreviated} - # Example: coll_gw0_a1b2c3d4_find_all_documents - collection_name = f"coll_{worker_id}_{name_hash}_{abbreviated}"[:100] # Collection name limit + collection_name = fixtures.generate_collection_name(full_test_id, worker_id) coll = database_client[collection_name] yield coll # Cleanup: drop collection - try: - coll.drop() - except Exception: - pass # Best effort cleanup + fixtures.cleanup_collection(database_client, collection_name) diff --git a/framework/__init__.py b/framework/__init__.py new file mode 100644 index 0000000..2f91d5c --- /dev/null +++ b/framework/__init__.py @@ -0,0 +1,11 @@ +""" +Reusable testing framework for DocumentDB functional tests. + +This framework provides: +- Assertion helpers for common test scenarios +- Fixture utilities for test isolation and database management +""" + +from . import assertions, fixtures + +__all__ = ["assertions", "fixtures"] \ No newline at end of file diff --git a/tests/common/assertions.py b/framework/assertions.py similarity index 99% rename from tests/common/assertions.py rename to framework/assertions.py index c6d9e89..c55a280 100644 --- a/tests/common/assertions.py +++ b/framework/assertions.py @@ -100,4 +100,4 @@ def assert_count(collection, filter_query: Dict, expected_count: int): assert actual_count == expected_count, ( f"Document count mismatch for filter {filter_query}. " f"Expected {expected_count}, got {actual_count}" - ) + ) \ No newline at end of file diff --git a/framework/fixtures.py b/framework/fixtures.py new file mode 100644 index 0000000..32590e9 --- /dev/null +++ b/framework/fixtures.py @@ -0,0 +1,123 @@ +""" +Fixture utilities for test isolation and database management. + +Provides reusable functions for creating database clients, generating unique +names, and managing test isolation. +""" + +import hashlib +from pymongo import MongoClient + + +def create_engine_client(connection_string: str, engine_name: str = "default"): + """ + Create and verify a MongoDB client connection. + + Args: + connection_string: MongoDB connection string + engine_name: Optional engine identifier for error messages + + Returns: + MongoClient: Connected MongoDB client + + Raises: + ConnectionError: If unable to connect to the database + """ + client = MongoClient(connection_string) + + # Verify connection + try: + client.admin.command("ping") + except Exception as e: + # Close the client before raising + client.close() + # Raise ConnectionError so analyzer categorizes as INFRA_ERROR + raise ConnectionError( + f"Cannot connect to {engine_name} at {connection_string}: {e}" + ) from e + + return client + + +def generate_database_name(test_id: str, worker_id: str = "master") -> str: + """ + Generate a unique database name for test isolation. + + Creates a collision-free name for parallel execution that includes + worker ID, hash, and abbreviated test name. + + Args: + test_id: Full test identifier (e.g., test file path + test name) + worker_id: Worker ID from pytest-xdist (e.g., 'gw0', 'gw1', or 'master') + + Returns: + str: Unique database name (max 63 characters for MongoDB compatibility) + """ + # Create a short hash for uniqueness (first 8 chars of SHA256) + name_hash = hashlib.sha256(test_id.encode()).hexdigest()[:8] + + # Get abbreviated test name for readability (sanitize and truncate) + test_name = test_id.split("::")[-1] if "::" in test_id else test_id + abbreviated = test_name.replace("[", "_").replace("]", "_")[:20] + + # Combine: test_{worker}_{hash}_{abbreviated} + # Example: test_gw0_a1b2c3d4_find_all_documents + db_name = f"test_{worker_id}_{name_hash}_{abbreviated}"[:63] # MongoDB limit + + return db_name + + +def generate_collection_name(test_id: str, worker_id: str = "master") -> str: + """ + Generate a unique collection name for test isolation. + + Creates a collision-free name for parallel execution that includes + worker ID, hash, and abbreviated test name. + + Args: + test_id: Full test identifier (e.g., test file path + test name) + worker_id: Worker ID from pytest-xdist (e.g., 'gw0', 'gw1', or 'master') + + Returns: + str: Unique collection name (max 100 characters) + """ + # Create a short hash for uniqueness (first 8 chars of SHA256) + name_hash = hashlib.sha256(test_id.encode()).hexdigest()[:8] + + # Get abbreviated test name for readability (sanitize and truncate) + test_name = test_id.split("::")[-1] if "::" in test_id else test_id + abbreviated = test_name.replace("[", "_").replace("]", "_")[:25] + + # Combine: coll_{worker}_{hash}_{abbreviated} + # Example: coll_gw0_a1b2c3d4_find_all_documents + collection_name = f"coll_{worker_id}_{name_hash}_{abbreviated}"[:100] # Collection name limit + + return collection_name + + +def cleanup_database(client: MongoClient, database_name: str): + """ + Drop a database, best effort. + + Args: + client: MongoDB client + database_name: Name of database to drop + """ + try: + client.drop_database(database_name) + except Exception: + pass # Best effort cleanup + + +def cleanup_collection(database, collection_name: str): + """ + Drop a collection, best effort. + + Args: + database: MongoDB database object + collection_name: Name of collection to drop + """ + try: + database[collection_name].drop() + except Exception: + pass # Best effort cleanup \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py index 16329b8..e23aa21 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -7,7 +7,7 @@ DocumentDB functionality. """ -from tests.common.assertions import ( +from framework.assertions import ( assert_count, assert_document_match, assert_documents_match, diff --git a/tests/common/__init__.py b/tests/common/__init__.py deleted file mode 100644 index b34d9ac..0000000 --- a/tests/common/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Common utilities and helpers for functional tests.""" diff --git a/tests/find/test_basic_queries.py b/tests/find/test_basic_queries.py index 3de9c70..748bc65 100644 --- a/tests/find/test_basic_queries.py +++ b/tests/find/test_basic_queries.py @@ -6,7 +6,7 @@ import pytest -from tests.common.assertions import assert_document_match +from framework.assertions import assert_document_match @pytest.mark.find diff --git a/tests/find/test_projections.py b/tests/find/test_projections.py index ab941bd..47d605a 100644 --- a/tests/find/test_projections.py +++ b/tests/find/test_projections.py @@ -6,7 +6,7 @@ import pytest -from tests.common.assertions import assert_field_exists, assert_field_not_exists +from framework.assertions import assert_field_exists, assert_field_not_exists @pytest.mark.find diff --git a/tests/insert/test_insert_operations.py b/tests/insert/test_insert_operations.py index ef8e056..3ff30ae 100644 --- a/tests/insert/test_insert_operations.py +++ b/tests/insert/test_insert_operations.py @@ -8,7 +8,7 @@ from bson import ObjectId from pymongo.errors import DuplicateKeyError -from tests.common.assertions import assert_count +from framework.assertions import assert_count @pytest.mark.insert