Skip to content

feat: implement support for mongodb #85

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: yugabyte
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/supported-databases/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This section provides detailed information on the supported databases, including
bigquery
cockroachdb
yugabyte
mongodb
redis
valkey
elasticsearch
Expand Down
77 changes: 77 additions & 0 deletions docs/supported-databases/mongodb.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
\
MongoDB
=======

Integration with `MongoDB <https://www.mongodb.com/>`_, a NoSQL document-oriented database.

This integration uses the official `PyMongo <https://pymongo.readthedocs.io/>`_ driver to interact with MongoDB.

Installation
------------

.. code-block:: bash

pip install pytest-databases[mongodb]


Usage Example
-------------

.. code-block:: python

import pytest
import pymongo
from pytest_databases.docker.mongodb import MongoDBService

pytest_plugins = ["pytest_databases.docker.mongodb"]

def test_mongodb_service(mongodb_service: MongoDBService) -> None:
client = pymongo.MongoClient(
host=mongodb_service.host,
port=mongodb_service.port,
username=mongodb_service.username,
password=mongodb_service.password,
)
# Ping the server to ensure connection
client.admin.command("ping")
client.close()

def test_mongodb_connection(mongodb_connection: pymongo.MongoClient) -> None:
# mongodb_connection is an instance of pymongo.MongoClient
# You can use it to interact with the database
db = mongodb_connection["mydatabase"]
collection = db["mycollection"]
collection.insert_one({"name": "test_document", "value": 1})
result = collection.find_one({"name": "test_document"})
assert result is not None
assert result["value"] == 1
# Clean up (optional, depending on your test needs)
collection.delete_one({"name": "test_document"})
mongodb_connection.close()

def test_mongodb_database(mongodb_database: pymongo.database.Database) -> None:
# mongodb_database is an instance of pymongo.database.Database
# This fixture provides a database that is unique per test function if xdist is used
# and xdist_mongodb_isolation_level is "database" (the default).
collection = mongodb_database["mycollection"]
collection.insert_one({"name": "another_document", "value": 2})
result = collection.find_one({"name": "another_document"})
assert result is not None
assert result["value"] == 2
# No need to close the database object explicitly, the connection is managed by mongodb_connection

Available Fixtures
------------------

* ``mongodb_service``: A fixture that provides a MongoDB service, giving access to connection details like host, port, username, and password.
* ``mongodb_connection``: A fixture that provides a ``pymongo.MongoClient`` instance connected to the MongoDB service.
* ``mongodb_database``: A fixture that provides a ``pymongo.database.Database`` instance.
* ``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.

Service API
-----------

.. automodule:: pytest_databases.docker.mongodb
:members: MongoDBService, _provide_mongodb_service
:undoc-members:
:show-inheritance:
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ keywords = [
"azure",
"valkey",
"dragonflydb",
"mongodb",
]
# options under https://pypi.org/classifiers/
classifiers = [
Expand All @@ -47,7 +48,7 @@ classifiers = [
"Programming Language :: Python :: Implementation :: PyPy",
]
# direct dependencies of this package
dependencies = ["pytest", "filelock", "docker"]
dependencies = ["pytest", "filelock", "docker", "pymongo"]

[project.urls]
Documentation = "https://github.com/litestar-org/pytest-databases#readme"
Expand All @@ -64,6 +65,7 @@ elasticsearch8 = ["elasticsearch8"]
keydb = ["redis"]
mariadb = ["mariadb"]
minio = ["minio"]
mongodb = ["pymongo"]
mssql = ["pymssql"]
mysql = ["mysql-connector-python"]
oracle = ["oracledb"]
Expand Down
135 changes: 135 additions & 0 deletions src/pytest_databases/docker/mongodb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from __future__ import annotations

import contextlib
import traceback
from collections.abc import Generator
from dataclasses import dataclass
from typing import TYPE_CHECKING

import pymongo
from pymongo.errors import ConnectionFailure
import pytest

from pytest_databases.helpers import get_xdist_worker_num
from pytest_databases.types import ServiceContainer, XdistIsolationLevel

if TYPE_CHECKING:
from collections.abc import Generator

from pytest_databases._service import DockerService
from pymongo import MongoClient
from pymongo.database import Database


@dataclass
class MongoDBService(ServiceContainer):
username: str
password: str
database: str


@pytest.fixture(scope="session")
def xdist_mongodb_isolation_level() -> XdistIsolationLevel:
return "database"


@contextlib.contextmanager
def _provide_mongodb_service(
docker_service: DockerService,
image: str,
name: str,
isolation_level: XdistIsolationLevel,
) -> Generator[MongoDBService, None, None]:
username = "mongo_user"
password = "mongo_password"
default_database_name = "pytest_db"

worker_num = get_xdist_worker_num()
database_name = f"{default_database_name}_{worker_num}" if worker_num is not None and isolation_level == "database" else default_database_name

def check(_service: ServiceContainer) -> bool:
client: MongoClient | None = None
try:
client = pymongo.MongoClient(
host=_service.host,
port=_service.port,
username=username,
password=password,
serverSelectionTimeoutMS=2000, # Increased timeout for robust check
connectTimeoutMS=2000,
socketTimeoutMS=2000,
)
client.admin.command("ping")
return True
except ConnectionFailure:
traceback.print_exc()
return False
finally:
if client:
client.close()

with docker_service.run(
image=image,
name=name,
container_port=27017, # Default MongoDB port
env={
"MONGO_INITDB_ROOT_USERNAME": username,
"MONGO_INITDB_ROOT_PASSWORD": password,
},
check=check,
pause=0.5, # Time for MongoDB to initialize
timeout=120, # Total timeout for service to be ready
) as service:
yield MongoDBService(
host=service.host,
port=service.port,
username=username,
password=password,
database=database_name
)


@pytest.fixture(autouse=False, scope="session")
def mongodb_image() -> str:
return "mongo:latest"


@pytest.fixture(autouse=False, scope="session")
def mongodb_service(
docker_service: DockerService,
xdist_mongodb_isolation_level: XdistIsolationLevel,
mongodb_image: str,
) -> Generator[MongoDBService, None, None]:
with _provide_mongodb_service(
docker_service, mongodb_image, "mongodb", xdist_mongodb_isolation_level
) as service:
yield service


@pytest.fixture(autouse=False, scope="session")
def mongodb_connection(mongodb_service: MongoDBService) -> Generator[MongoClient, None, None]:
client: MongoClient | None = None
try:
client = pymongo.MongoClient(
host=mongodb_service.host,
port=mongodb_service.port,
username=mongodb_service.username,
password=mongodb_service.password,
)
yield client
finally:
if client:
client.close()


@pytest.fixture(autouse=False, scope="function")
def mongodb_database(
mongodb_connection: MongoClient,
mongodb_service: MongoDBService
) -> Generator[Database, None, None]:
"""Provides a MongoDB database instance for testing."""
db = mongodb_connection[mongodb_service.database]
yield db
# For a truly clean state per test, you might consider dropping the database here,
# but it depends on the desired test isolation and speed.
# e.g., mongodb_connection.drop_database(mongodb_service.database)
104 changes: 104 additions & 0 deletions tests/test_mongodb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from __future__ import annotations

import pytest


@pytest.mark.parametrize(
"service_fixture",
[
"mongodb_service",
],
)
def test_service_fixture(pytester: pytest.Pytester, service_fixture: str) -> None:
pytester.makepyfile(f"""
import pymongo
pytest_plugins = ["pytest_databases.docker.mongodb"]

def test({service_fixture}):
client = pymongo.MongoClient(
host={service_fixture}.host,
port={service_fixture}.port,
username={service_fixture}.username,
password={service_fixture}.password,
)
client.admin.command("ping")
""")

result = pytester.runpytest("-vv")
result.assert_outcomes(passed=1)


@pytest.mark.parametrize(
"connection_fixture",
[
"mongodb_connection",
],
)
def test_connection_fixture(pytester: pytest.Pytester, connection_fixture: str) -> None:
pytester.makepyfile(f"""
pytest_plugins = ["pytest_databases.docker.mongodb"]

def test({connection_fixture}):
db = {connection_fixture}["test_db"]
collection = db["test_collection"]
collection.insert_one({{"key": "value"}})
result = collection.find_one({{"key": "value"}})
assert result is not None and result["key"] == "value"
""")

result = pytester.runpytest("-vv")
result.assert_outcomes(passed=1)


def test_xdist_isolate_database(pytester: pytest.Pytester) -> None:
pytester.makepyfile("""
pytest_plugins = ["pytest_databases.docker.mongodb"]

def test_1(mongodb_database):
collection = mongodb_database["test_collection"]
collection.insert_one({{"key": "value1"}})
result = collection.find_one({{"key": "value1"}})
assert result is not None and result["key"] == "value1"

def test_2(mongodb_database):
collection = mongodb_database["test_collection"]
# If isolation is working, this collection should be empty or not exist
result = collection.find_one({{"key": "value1"}})
assert result is None
collection.insert_one({{"key": "value2"}})
result = collection.find_one({{"key": "value2"}})
assert result is not None and result["key"] == "value2"
""")

result = pytester.runpytest("-n", "2")
result.assert_outcomes(passed=2)


def test_xdist_isolate_server(pytester: pytest.Pytester) -> None:
pytester.makepyfile("""
import pytest
pytest_plugins = ["pytest_databases.docker.mongodb"]

@pytest.fixture(scope="session")
def xdist_mongodb_isolation_level():
return "server"

def test_1(mongodb_connection):
# Operations in one test should not affect the other if server isolation is working,
# as they would be on different MongoDB server instances.
db = mongodb_connection["test_db_server_1"]
collection = db["test_collection"]
collection.insert_one({{"key": "server1"}})
assert collection.count_documents({}) == 1

def test_2(mongodb_connection):
db = mongodb_connection["test_db_server_2"] # Different DB name to be sure
collection = db["test_collection"]
# This count should be 0 if it's a new server instance
assert collection.count_documents({}) == 0
collection.insert_one({{"key": "server2"}})
assert collection.count_documents({}) == 1
""")

result = pytester.runpytest("-n", "2")
result.assert_outcomes(passed=2)
Loading
Loading