Use Case 14: Testing Guide – Mocking Motor & Population Engine
Scenario:
You want to write unit tests for repositories that use async Motor collections and the PopulationEngine without connecting to a real MongoDB instance.
📦 What’s New?
| Component | Description |
|---|---|
AsyncMock for collections |
Allows you to stub find_one, insert_one, etc., and control returned data. |
Patching MongoConnectionManager.get_database |
Redirects repository initialization to a mock collection. |
Mocking PopulationEngine |
Replace the real engine with a lightweight stub that returns pre‑crafted objects. |
| Example test files | tests/test_populating_repository.py and tests/test_transactions.py are used as reference implementations. |
🚀 Example Test Boilerplate
Python
import pytest
from unittest.mock import AsyncMock, patch
from bson import ObjectId
from mongo_ops.models import BaseDocument
from mongo_ops.populate import PopulateRule, PopulationEngine
from mongo_ops.repository import PopulatingRepository
# ----------------------------------------------------------------------
# 1️⃣ Define simple models (same as in the library tests)
# ----------------------------------------------------------------------
class Profile(BaseDocument):
avatar_url: str = ""
class User(BaseDocument):
name: str = ""
profile_id: ObjectId | None = None
profile: Profile | None = None
# ----------------------------------------------------------------------
# 2️⃣ Fixture – mock Motor collection
# ----------------------------------------------------------------------
@pytest.fixture
def mock_collection():
return AsyncMock()
# ----------------------------------------------------------------------
# 3️⃣ Fixture – mock PopulationEngine (optional)
# ----------------------------------------------------------------------
@pytest.fixture
def engine():
# Provide a real engine but replace the repo for "profiles" with a mock
profile_repo = AsyncMock()
return PopulationEngine({"profiles": profile_repo})
# ----------------------------------------------------------------------
# 4️⃣ Fixture – repository under test
# ----------------------------------------------------------------------
@pytest.fixture
def repo(mock_collection, engine):
with patch("mongo_ops.repository.MongoConnectionManager.get_database") as mock_db:
mock_db.return_value.__getitem__.return_value = mock_collection
repo = PopulatingRepository(
"users",
User,
population_engine=engine,
populate_rules=[
PopulateRule(
field_name="profile",
collection_name="profiles",
ref_field="profile_id",
)
],
)
repo.collection = mock_collection
return repo
# ----------------------------------------------------------------------
# 5️⃣ Test – `get_by_id` populates the related document
# ----------------------------------------------------------------------
@pytest.mark.asyncio
async def test_get_by_id_populates(repo, mock_collection, engine):
# Arrange – user document returned from the DB
uid = ObjectId()
pid = ObjectId()
mock_collection.find_one.return_value = {
"_id": uid,
"name": "Alice",
"profile_id": pid,
"created_at": "2024-01-01T00:00:00",
"updated_at": "2024-01-01T00:00:00",
}
# Mock the profile repo to return a concrete Profile instance
engine._repos["profiles"].get_by_id.return_value = Profile(id=pid, avatar_url="pic.png")
# Act
result = await repo.get_by_id(uid)
# Assert
assert result is not None
assert result.name == "Alice"
assert result.profile is not None
assert result.profile.avatar_url == "pic.png"
# ----------------------------------------------------------------------
# 6️⃣ Test – transaction helper uses the real `TransactionManager`
# ----------------------------------------------------------------------
from mongo_ops.transactions import TransactionManager
@pytest.mark.asyncio
async def test_execute_transaction(monkeypatch):
# Fake a Motor client session that records calls
async def fake_op(session):
return "ok"
# Execute with the helper – it should return a list with the result
results = await TransactionManager.execute_transaction([fake_op])
assert results == ["ok"]
💡 Tips
- Never hit the network: All DB calls are mocked, so tests run instantly.
- Reuse fixtures: Keep the
mock_collectionandenginefixtures in aconftest.pyfile for other repo tests. - Coverage: The same pattern works for
CachedBaseRepository– just mockcache_backendmethods (get,set,delete). - AsyncTestCase: If you prefer
unitteststyle, useIsolatedAsyncioTestCasefrom Python 3.8+.