Skip to content

Commit 6d491a2

Browse files
authored
feat: implement support for mongodb (#85)
1 parent 8ddf196 commit 6d491a2

File tree

7 files changed

+443
-1
lines changed

7 files changed

+443
-1
lines changed

docs/conf.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@
6363
"exclude-members": "__weakref__",
6464
}
6565

66+
# Mock imports for modules not needed during doc generation
67+
autodoc_mock_imports = ["OpenSSL"]
68+
6669
# Intersphinx settings
6770
intersphinx_mapping = {
6871
"python": ("https://docs.python.org/3", None),

docs/supported-databases/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ This section provides detailed information on the supported databases, including
1515
bigquery
1616
cockroachdb
1717
yugabyte
18+
mongodb
1819
gizmosql
1920
redis
2021
valkey
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
MongoDB
2+
=======
3+
4+
Integration with `MongoDB <https://www.mongodb.com/>`_, a NoSQL document-oriented database.
5+
6+
This integration uses the official `PyMongo <https://pymongo.readthedocs.io/>`_ driver to interact with MongoDB.
7+
8+
Installation
9+
------------
10+
11+
.. code-block:: bash
12+
13+
pip install pytest-databases[mongodb]
14+
15+
16+
Usage Example
17+
-------------
18+
19+
.. code-block:: python
20+
21+
import pytest
22+
import pymongo
23+
from pytest_databases.docker.mongodb import MongoDBService
24+
25+
pytest_plugins = ["pytest_databases.docker.mongodb"]
26+
27+
def test_mongodb_service(mongodb_service: MongoDBService) -> None:
28+
client = pymongo.MongoClient(
29+
host=mongodb_service.host,
30+
port=mongodb_service.port,
31+
username=mongodb_service.username,
32+
password=mongodb_service.password,
33+
)
34+
# Ping the server to ensure connection
35+
client.admin.command("ping")
36+
client.close()
37+
38+
def test_mongodb_connection(mongodb_connection: pymongo.MongoClient) -> None:
39+
# mongodb_connection is an instance of pymongo.MongoClient
40+
# You can use it to interact with the database
41+
db = mongodb_connection["mydatabase"]
42+
collection = db["mycollection"]
43+
collection.insert_one({"name": "test_document", "value": 1})
44+
result = collection.find_one({"name": "test_document"})
45+
assert result is not None
46+
assert result["value"] == 1
47+
# Clean up (optional, depending on your test needs)
48+
collection.delete_one({"name": "test_document"})
49+
mongodb_connection.close()
50+
51+
def test_mongodb_database(mongodb_database: pymongo.database.Database) -> None:
52+
# mongodb_database is an instance of pymongo.database.Database
53+
# This fixture provides a database that is unique per test function if xdist is used
54+
# and xdist_mongodb_isolation_level is "database" (the default).
55+
collection = mongodb_database["mycollection"]
56+
collection.insert_one({"name": "another_document", "value": 2})
57+
result = collection.find_one({"name": "another_document"})
58+
assert result is not None
59+
assert result["value"] == 2
60+
# No need to close the database object explicitly, the connection is managed by mongodb_connection
61+
62+
Available Fixtures
63+
------------------
64+
65+
* ``mongodb_service``: A fixture that provides a MongoDB service, giving access to connection details like host, port, username, and password.
66+
* ``mongodb_connection``: A fixture that provides a ``pymongo.MongoClient`` instance connected to the MongoDB service.
67+
* ``mongodb_database``: A fixture that provides a ``pymongo.database.Database`` instance.
68+
* ``mongodb_image``: A fixture that returns the Docker image name used for the MongoDB service (default: "mongo:latest"). You can override this fixture to use a different MongoDB version.
69+
70+
Service API
71+
-----------
72+
73+
.. automodule:: pytest_databases.docker.mongodb
74+
:members: MongoDBService, _provide_mongodb_service
75+
:undoc-members:
76+
:show-inheritance:

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ keywords = [
3333
"azure",
3434
"valkey",
3535
"dragonflydb",
36+
"mongodb",
3637
"gizmosql",
3738
"flightsql",
3839
]
@@ -67,6 +68,7 @@ gizmosql = ["adbc-driver-flightsql", "pyarrow"]
6768
keydb = ["redis"]
6869
mariadb = ["mariadb"]
6970
minio = ["minio"]
71+
mongodb = ["pymongo"]
7072
mssql = ["pymssql"]
7173
mysql = ["mysql-connector-python"]
7274
oracle = ["oracledb"]
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
from __future__ import annotations
2+
3+
import contextlib
4+
import traceback
5+
from collections.abc import Generator
6+
from dataclasses import dataclass
7+
from typing import TYPE_CHECKING
8+
9+
import pymongo
10+
import pytest
11+
from pymongo.errors import ConnectionFailure
12+
13+
from pytest_databases.helpers import get_xdist_worker_num
14+
from pytest_databases.types import ServiceContainer, XdistIsolationLevel
15+
16+
if TYPE_CHECKING:
17+
from collections.abc import Generator
18+
19+
from pymongo import MongoClient
20+
from pymongo.database import Database
21+
22+
from pytest_databases._service import DockerService
23+
24+
25+
@dataclass
26+
class MongoDBService(ServiceContainer):
27+
username: str
28+
password: str
29+
database: str
30+
31+
32+
@pytest.fixture(scope="session")
33+
def xdist_mongodb_isolation_level() -> XdistIsolationLevel:
34+
return "database"
35+
36+
37+
@contextlib.contextmanager
38+
def _provide_mongodb_service(
39+
docker_service: DockerService,
40+
image: str,
41+
name: str,
42+
isolation_level: XdistIsolationLevel,
43+
) -> Generator[MongoDBService, None, None]:
44+
username = "mongo_user"
45+
password = "mongo_password"
46+
default_database_name = "pytest_db"
47+
48+
container_name = name
49+
database_name = default_database_name
50+
worker_num = get_xdist_worker_num()
51+
if worker_num is not None:
52+
suffix = f"_{worker_num}"
53+
if isolation_level == "server":
54+
container_name += suffix
55+
else:
56+
database_name += suffix
57+
58+
def check(_service: ServiceContainer) -> bool:
59+
client: MongoClient | None = None
60+
try:
61+
client = pymongo.MongoClient(
62+
host=_service.host,
63+
port=_service.port,
64+
username=username,
65+
password=password,
66+
serverSelectionTimeoutMS=2000, # Increased timeout for robust check
67+
connectTimeoutMS=2000,
68+
socketTimeoutMS=2000,
69+
)
70+
client.admin.command("ping")
71+
except ConnectionFailure:
72+
traceback.print_exc()
73+
return False
74+
else:
75+
return True
76+
finally:
77+
if client:
78+
client.close()
79+
80+
with docker_service.run(
81+
image=image,
82+
name=container_name,
83+
container_port=27017,
84+
env={
85+
"MONGO_INITDB_ROOT_USERNAME": username,
86+
"MONGO_INITDB_ROOT_PASSWORD": password,
87+
},
88+
check=check,
89+
pause=0.5,
90+
timeout=120,
91+
transient=isolation_level == "server",
92+
) as service:
93+
yield MongoDBService(
94+
host=service.host, port=service.port, username=username, password=password, database=database_name
95+
)
96+
97+
98+
@pytest.fixture(autouse=False, scope="session")
99+
def mongodb_image() -> str:
100+
return "mongo:latest"
101+
102+
103+
@pytest.fixture(autouse=False, scope="session")
104+
def mongodb_service(
105+
docker_service: DockerService,
106+
xdist_mongodb_isolation_level: XdistIsolationLevel,
107+
mongodb_image: str,
108+
) -> Generator[MongoDBService, None, None]:
109+
with _provide_mongodb_service(docker_service, mongodb_image, "mongodb", xdist_mongodb_isolation_level) as service:
110+
yield service
111+
112+
113+
@pytest.fixture(autouse=False, scope="session")
114+
def mongodb_connection(mongodb_service: MongoDBService) -> Generator[MongoClient, None, None]:
115+
client: MongoClient | None = None
116+
try:
117+
client = pymongo.MongoClient(
118+
host=mongodb_service.host,
119+
port=mongodb_service.port,
120+
username=mongodb_service.username,
121+
password=mongodb_service.password,
122+
)
123+
yield client
124+
finally:
125+
if client:
126+
client.close()
127+
128+
129+
@pytest.fixture(autouse=False, scope="function")
130+
def mongodb_database(
131+
mongodb_connection: MongoClient, mongodb_service: MongoDBService
132+
) -> Generator[Database, None, None]:
133+
"""Provides a MongoDB database instance for testing.
134+
135+
Yields:
136+
A MongoDB database instance.
137+
"""
138+
db = mongodb_connection[mongodb_service.database]
139+
yield db
140+
# For a truly clean state per test, you might consider dropping the database here,
141+
# but it depends on the desired test isolation and speed.
142+
# e.g., mongodb_connection.drop_database(mongodb_service.database)

tests/test_mongodb.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
from __future__ import annotations
2+
3+
import pytest
4+
5+
6+
@pytest.mark.parametrize(
7+
"service_fixture",
8+
[
9+
"mongodb_service",
10+
],
11+
)
12+
def test_service_fixture(pytester: pytest.Pytester, service_fixture: str) -> None:
13+
pytester.makepyfile(f"""
14+
import pymongo
15+
pytest_plugins = ["pytest_databases.docker.mongodb"]
16+
17+
def test({service_fixture}):
18+
client = pymongo.MongoClient(
19+
host={service_fixture}.host,
20+
port={service_fixture}.port,
21+
username={service_fixture}.username,
22+
password={service_fixture}.password,
23+
)
24+
client.admin.command("ping")
25+
""")
26+
27+
result = pytester.runpytest_subprocess("-p", "pytest_databases")
28+
result.assert_outcomes(passed=1)
29+
30+
31+
@pytest.mark.parametrize(
32+
"connection_fixture",
33+
[
34+
"mongodb_connection",
35+
],
36+
)
37+
def test_connection_fixture(pytester: pytest.Pytester, connection_fixture: str) -> None:
38+
pytester.makepyfile(f"""
39+
pytest_plugins = ["pytest_databases.docker.mongodb"]
40+
41+
def test({connection_fixture}):
42+
db = {connection_fixture}["test_db"]
43+
collection = db["test_collection"]
44+
collection.insert_one({{"key": "value"}})
45+
result = collection.find_one({{"key": "value"}})
46+
assert result is not None and result["key"] == "value"
47+
""")
48+
49+
result = pytester.runpytest_subprocess("-p", "pytest_databases")
50+
result.assert_outcomes(passed=1)
51+
52+
53+
def test_xdist_isolate_database(pytester: pytest.Pytester) -> None:
54+
pytester.makepyfile("""
55+
pytest_plugins = ["pytest_databases.docker.mongodb"]
56+
57+
def test_1(mongodb_database):
58+
collection = mongodb_database["test_collection"]
59+
collection.insert_one({"key": "value1"})
60+
result = collection.find_one({"key": "value1"})
61+
assert result is not None and result["key"] == "value1"
62+
63+
def test_2(mongodb_database):
64+
collection = mongodb_database["test_collection"]
65+
# If isolation is working, this collection should be empty or not exist
66+
result = collection.find_one({"key": "value1"})
67+
assert result is None
68+
collection.insert_one({"key": "value2"})
69+
result = collection.find_one({"key": "value2"})
70+
assert result is not None and result["key"] == "value2"
71+
""")
72+
73+
result = pytester.runpytest_subprocess("-p", "pytest_databases", "-n", "2")
74+
result.assert_outcomes(passed=2)
75+
76+
77+
def test_xdist_isolate_server(pytester: pytest.Pytester) -> None:
78+
pytester.makepyfile("""
79+
import pytest
80+
pytest_plugins = ["pytest_databases.docker.mongodb"]
81+
82+
@pytest.fixture(scope="session")
83+
def xdist_mongodb_isolation_level():
84+
return "server"
85+
86+
def test_1(mongodb_connection):
87+
# Operations in one test should not affect the other if server isolation is working,
88+
# as they would be on different MongoDB server instances.
89+
db = mongodb_connection["test_db_server_1"]
90+
collection = db["test_collection"]
91+
collection.insert_one({"key": "server1"})
92+
assert collection.count_documents({}) == 1
93+
94+
def test_2(mongodb_connection):
95+
db = mongodb_connection["test_db_server_2"] # Different DB name to be sure
96+
collection = db["test_collection"]
97+
# This count should be 0 if it's a new server instance
98+
assert collection.count_documents({}) == 0
99+
collection.insert_one({"key": "server2"})
100+
assert collection.count_documents({}) == 1
101+
""")
102+
103+
result = pytester.runpytest_subprocess("-p", "pytest_databases", "-n", "2")
104+
result.assert_outcomes(passed=2)

0 commit comments

Comments
 (0)