From b1394827719fe9cea94fa3aebea09bee2b050ae7 Mon Sep 17 00:00:00 2001 From: Casey Clements Date: Mon, 27 Apr 2026 15:57:43 -0400 Subject: [PATCH 1/3] feat(mongodb): add MongoDBAtlasLocalContainer Add a testcontainer for MongoDB Atlas Local (mongodb/mongodb-atlas-local), which provides a local Atlas environment with Atlas Search and Vector Search. - Extends DockerContainer directly (no auth required) - Uses ExecWaitStrategy with 'runner healthcheck' for readiness - Connection string includes directConnection=true - Includes integration tests for insert/query and connection string format --- .../testcontainers/mongodb/__init__.py | 59 +++++++++++++++++++ modules/mongodb/tests/test_mongodb.py | 21 ++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/modules/mongodb/testcontainers/mongodb/__init__.py b/modules/mongodb/testcontainers/mongodb/__init__.py index 7ab4e11d4..967890b7b 100644 --- a/modules/mongodb/testcontainers/mongodb/__init__.py +++ b/modules/mongodb/testcontainers/mongodb/__init__.py @@ -16,8 +16,10 @@ from pymongo import MongoClient +from testcontainers.core.container import DockerContainer from testcontainers.core.generic import DbContainer from testcontainers.core.utils import raise_for_deprecated_parameter +from testcontainers.core.wait_strategies import ExecWaitStrategy from testcontainers.core.waiting_utils import wait_for_logs @@ -87,3 +89,60 @@ def predicate(text: str) -> bool: def get_connection_client(self) -> MongoClient: return MongoClient(self.get_connection_url()) + + +class MongoDBAtlasLocalContainer(DockerContainer): + """ + MongoDB Atlas Local container for testing Atlas-specific features + such as Atlas Search and Vector Search. + + This container uses the ``mongodb/mongodb-atlas-local`` Docker image which + provides a fully functional local MongoDB Atlas deployment including a + single-node replica set and the MongoT search indexing service. + + Example: + + .. doctest:: + + >>> from testcontainers.mongodb import MongoDBAtlasLocalContainer + + >>> with MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:8.0.4") as atlas: + ... client = atlas.get_connection_client() + ... db = client.test + ... result = db.my_collection.insert_one({"key": "value"}) + ... found = db.my_collection.find_one({"key": "value"}) + ... found["key"] + 'value' + """ + + def __init__( + self, + image: str = "mongodb/mongodb-atlas-local:latest", + port: int = 27017, + **kwargs, + ) -> None: + super().__init__( + image=image, + _wait_strategy=ExecWaitStrategy(["runner", "healthcheck"]).with_startup_timeout(120), + **kwargs, + ) + self.port = port + self.with_exposed_ports(self.port) + + def get_connection_string(self) -> str: + """Get a MongoDB connection string with ``directConnection=true``. + + Returns: + A connection string of the form ``mongodb://host:port/?directConnection=true``. + """ + host = self.get_container_host_ip() + port = self.get_exposed_port(self.port) + return f"mongodb://{host}:{port}/?directConnection=true" + + def get_connection_client(self) -> MongoClient: + """Get a :class:`pymongo.MongoClient` connected to the container. + + Returns: + A ``MongoClient`` instance. + """ + return MongoClient(self.get_connection_string()) diff --git a/modules/mongodb/tests/test_mongodb.py b/modules/mongodb/tests/test_mongodb.py index 9bf3600f2..eaaecfef5 100644 --- a/modules/mongodb/tests/test_mongodb.py +++ b/modules/mongodb/tests/test_mongodb.py @@ -2,7 +2,7 @@ from pymongo import MongoClient from pymongo.errors import OperationFailure -from testcontainers.mongodb import MongoDbContainer +from testcontainers.mongodb import MongoDbContainer, MongoDBAtlasLocalContainer @pytest.mark.parametrize("version", ["7.0.7", "6.0.14", "5.0.26"]) @@ -51,3 +51,22 @@ def test_quoted_password(): expected_url = f"mongodb://{user}:{quoted_password}@{host}:{port}" url = container.get_connection_url() assert url == expected_url + + +def test_docker_run_mongodb_atlas_local(): + with MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:8.0.4") as atlas: + client = atlas.get_connection_client() + db = client.test + doc = {"name": "Atlas Test", "value": 42} + result = db.test_collection.insert_one(doc) + assert result.inserted_id + found = db.test_collection.find_one({"name": "Atlas Test"}) + assert found is not None + assert found["value"] == 42 + + +def test_mongodb_atlas_local_connection_string(): + with MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:8.0.4") as atlas: + conn_str = atlas.get_connection_string() + assert conn_str.startswith("mongodb://") + assert "directConnection=true" in conn_str From 3fcc9597c43dcb2c4e83fa13eba3b82f21645151 Mon Sep 17 00:00:00 2001 From: Casey Clements Date: Tue, 28 Apr 2026 18:03:32 -0400 Subject: [PATCH 2/3] test(mongodb): add vector search integration test for MongoDBAtlasLocalContainer - Add test_vector_search using deterministic hash-based mock embeddings - indexed_collection fixture: inserts docs, creates vector index, polls until all documents are indexed (handles OperationFailure during init) - Asserts top_k limiting, exact-match ranking, and score range Signed-off-by: Casey Clements --- modules/mongodb/tests/test_mongodb.py | 122 ++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/modules/mongodb/tests/test_mongodb.py b/modules/mongodb/tests/test_mongodb.py index eaaecfef5..5b7ef5906 100644 --- a/modules/mongodb/tests/test_mongodb.py +++ b/modules/mongodb/tests/test_mongodb.py @@ -1,6 +1,10 @@ +import hashlib +import time + import pytest from pymongo import MongoClient from pymongo.errors import OperationFailure +from pymongo.operations import SearchIndexModel from testcontainers.mongodb import MongoDbContainer, MongoDBAtlasLocalContainer @@ -70,3 +74,121 @@ def test_mongodb_atlas_local_connection_string(): conn_str = atlas.get_connection_string() assert conn_str.startswith("mongodb://") assert "directConnection=true" in conn_str + + +# --------------------------------------------------------------------------- +# Vector Search +# --------------------------------------------------------------------------- + +EMBEDDING_DIM = 16 +NUM_DOCS = 5 +SENTENCES = [ + "The quick brown fox jumps over the lazy dog", + "MongoDB Atlas provides powerful search capabilities", + "Vector search enables semantic similarity matching", + "Testcontainers make integration testing easy", + "Python is a versatile programming language", +] + + +def mock_embedding(text: str) -> list[float]: + """Deterministic mock embedding: hash the text into a float vector.""" + digest = hashlib.sha256(text.encode()).digest() + # Take EMBEDDING_DIM bytes and normalise to [0, 1] + return [b / 255.0 for b in digest[:EMBEDDING_DIM]] + + +@pytest.fixture(scope="module") +def atlas(): + with MongoDBAtlasLocalContainer("mongodb/mongodb-atlas-local:8.0.4") as container: + yield container + + +@pytest.fixture(scope="module") +def indexed_collection(atlas): + """Insert documents with embeddings, create a vector index, and wait until all docs are indexed.""" + client = atlas.get_connection_client() + db = client["test_vector"] + collection = db["documents"] + + # Insert documents + docs = [{"text": s, "embedding": mock_embedding(s)} for s in SENTENCES] + collection.insert_many(docs) + + # Create vector search index + index_definition = { + "fields": [ + { + "type": "vector", + "path": "embedding", + "numDimensions": EMBEDDING_DIM, + "similarity": "cosine", + } + ] + } + collection.create_search_index( + model=SearchIndexModel( + definition=index_definition, + name="vector_index", + type="vectorSearch", + ) + ) + + # Wait until all documents are indexed by polling $vectorSearch. + # The index may throw OperationFailure while it is still initialising. + deadline = time.monotonic() + 60 + while time.monotonic() < deadline: + try: + results = list( + collection.aggregate( + [ + { + "$vectorSearch": { + "index": "vector_index", + "path": "embedding", + "queryVector": mock_embedding("test"), + "numCandidates": NUM_DOCS, + "limit": NUM_DOCS, + } + } + ] + ) + ) + except OperationFailure: + # Index not yet initialised + results = [] + if len(results) == NUM_DOCS: + break + time.sleep(1) + else: + pytest.fail(f"Vector index did not index all {NUM_DOCS} documents within 60s") + + return collection + + +def test_vector_search(indexed_collection): + """Search for fewer documents than exist and verify the count.""" + top_k = 3 + query_vector = mock_embedding(SENTENCES[0]) + results = list( + indexed_collection.aggregate( + [ + { + "$vectorSearch": { + "index": "vector_index", + "path": "embedding", + "queryVector": query_vector, + "numCandidates": NUM_DOCS, + "limit": top_k, + } + }, + {"$addFields": {"score": {"$meta": "vectorSearchScore"}}}, + ] + ) + ) + assert len(results) == top_k + # The first result should be the sentence itself (exact match → highest score) + assert results[0]["text"] == SENTENCES[0] + # All results should have a score + for doc in results: + assert 0.0 <= doc["score"] <= 1.0 From 04e158b6894cc041ae5b10b3ab94fe896d8a11a6 Mon Sep 17 00:00:00 2001 From: Casey Clements Date: Tue, 28 Apr 2026 18:26:04 -0400 Subject: [PATCH 3/3] docs(mongodb): add MongoDBAtlasLocalContainer to README and docs - Add autoclass directive in modules/mongodb/README.rst - Update docs/modules/mongodb.md to describe both container classes Signed-off-by: Casey Clements --- docs/modules/mongodb.md | 5 +++++ modules/mongodb/README.rst | 1 + 2 files changed, 6 insertions(+) diff --git a/docs/modules/mongodb.md b/docs/modules/mongodb.md index 0c2d2d75d..9c3480dde 100644 --- a/docs/modules/mongodb.md +++ b/docs/modules/mongodb.md @@ -6,6 +6,11 @@ Since testcontainers-python