Advanced Mocking Patterns for Backend Testing¶
Overview¶
This document describes critical mocking patterns discovered during Issue #67 implementation. These patterns solve common testing challenges when dealing with: - Services with complex dependency injection - Singleton services with local imports - MongoDB service mocking
Context: These patterns emerged after 12 failed test attempts using standard pytest fixtures and module-level patching. The solutions documented here are battle-tested and eliminate common "Connexion MongoDB non établie" errors.
Pattern 1: Helper Function for Services with Dependency Injection¶
Problem¶
Testing services that initialize dependencies in __init__() or use attribute assignment is difficult with pytest fixtures when methods use local imports.
# ❌ PROBLEMATIC: Pytest fixture approach
@pytest.fixture
def collections_service(mock_mongodb_service):
with patch.object(CollectionsManagementService, "mongodb_service", mock_mongodb_service):
service = CollectionsManagementService()
return service
Why this fails:
- Services use local imports inside methods (e.g., from bson import ObjectId)
- Patching at module level doesn't intercept calls after service instantiation
- Dependency injection happens in __init__, but method-level imports bypass mocks
Solution: Create Mocked Service Helper¶
def create_mocked_service():
"""Creates service with all mocks configured.
Recommended pattern for testing services with injected dependencies.
Avoids pytest fixture issues with patch.object().
"""
from bson import ObjectId
service = CollectionsManagementService()
# Mock mongodb_service directly on instance
mock_mongodb = Mock()
mock_mongodb.create_author_if_not_exists.return_value = ObjectId("507f1f77bcf86cd799439014")
mock_mongodb.create_book_if_not_exists.return_value = ObjectId("507f1f77bcf86cd799439015")
mock_mongodb.update_avis_critique = Mock(return_value=True)
mock_mongodb.get_avis_critique_by_id.return_value = None
# Mock MongoDB collections
mock_mongodb.avis_critiques_collection = Mock()
mock_mongodb.livres_collection = Mock()
# Direct injection (bypasses __init__)
service.mongodb_service = mock_mongodb
service._mock_mongodb = mock_mongodb # For test access
return service
Usage in Tests¶
def test_should_update_summary():
# Arrange
service = create_mocked_service()
# Configure mock for this specific test
service._mock_mongodb.get_avis_critique_by_id.return_value = {
"_id": "507f...",
"summary": "Original text"
}
book_data = {"auteur": "Author", "titre": "Title"}
# Act
service.handle_book_validation(book_data)
# Assert
assert service._mock_mongodb.update_avis_critique.called
When to Use¶
✅ Use helper functions when:
- Service has multiple dependencies (MongoDB, cache, external APIs)
- Dependencies set via __init__ or attribute assignment
- Methods use local imports (from bson import ObjectId)
- Need fine-grained control over mock configuration per test
❌ Use fixtures when: - Mocking simple external modules (no dependency injection) - Setting up test data (dictionaries, lists, constants) - Sharing configuration across multiple tests - No complex service initialization
Pattern 2: Singleton Service with Local Imports¶
Problem¶
CRITICAL: Standard module-level patching does NOT work for singleton services imported locally inside methods.
# ❌ PROBLEMATIC: Service method with local import
class CollectionsManagementService:
def handle_book_validation(self, book_data):
# Local import inside method
from ..services.livres_auteurs_cache_service import livres_auteurs_cache_service
# This calls the REAL singleton, not the mock!
livres_auteurs_cache_service.mark_as_processed(cache_id, author_id, book_id)
Why module-level patching fails:
# ❌ This doesn't work
def test_something():
with patch("back_office_lmelp.services.collections_management_service.livres_auteurs_cache_service"):
service.handle_book_validation(book_data) # Import happens here, bypasses mock!
What happens:
1. Test starts, applies patch to module namespace
2. Method executes from ..services.livres_auteurs_cache_service import livres_auteurs_cache_service
3. Python resolves import from original module, not patched namespace
4. Real singleton instance is used, tries to access real MongoDB
5. Error: "Connexion MongoDB non établie"
Solution: Patch Global Singleton Instance¶
✅ GOOD: Patch methods on the global instance, not the module
def patch_cache_service(is_already_corrected=False):
"""Returns tuple of patches for livres_auteurs_cache_service singleton.
Usage:
patches = patch_cache_service()
with patches[0], patches[1], patches[2]:
# test code
"""
from unittest.mock import patch as mock_patch
return (
mock_patch("back_office_lmelp.services.livres_auteurs_cache_service.livres_auteurs_cache_service.mark_as_processed", return_value=True),
mock_patch("back_office_lmelp.services.livres_auteurs_cache_service.livres_auteurs_cache_service.is_summary_corrected", return_value=is_already_corrected),
mock_patch("back_office_lmelp.services.livres_auteurs_cache_service.livres_auteurs_cache_service.mark_summary_corrected", return_value=True),
)
Usage in Tests¶
def test_with_singleton_service():
service = create_mocked_service()
book_data = {...}
# Apply all patches from helper
patches = patch_cache_service()
with patches[0], patches[1], patches[2]:
service.handle_book_validation(book_data)
# Assertions
assert service._mock_mongodb.update_avis_critique.called
Why Tuple of Patches?¶
Python's with statement requires each patch to be applied individually:
# ✅ Correct: Unpack tuple manually
patches = patch_cache_service()
with patches[0], patches[1], patches[2]:
# test code
# ❌ Wrong: Can't use tuple directly
with patch_cache_service(): # TypeError
# test code
When to Use¶
✅ Use singleton instance patching when:
- Service uses from ..module import singleton_instance inside methods
- Standard module patching fails with "not connected" or "not initialized" errors
- Imported object is a singleton instance (not a class)
- Singleton instantiated at module level (service = Service())
❌ Use standard module patching when: - Imports happen at module level (top of file) - Importing a class, not an instance - No local imports inside methods
Complete Example: Issue #67 Summary Correction Tests¶
Test File Structure¶
# File: tests/test_avis_critique_summary_correction.py
from unittest.mock import Mock, patch
from bson import ObjectId
from back_office_lmelp.services.collections_management_service import CollectionsManagementService
def create_mocked_service():
"""Helper: Create service with mocked MongoDB dependencies."""
service = CollectionsManagementService()
mock_mongodb = Mock()
mock_mongodb.create_author_if_not_exists.return_value = ObjectId("507f1f77bcf86cd799439014")
mock_mongodb.create_book_if_not_exists.return_value = ObjectId("507f1f77bcf86cd799439015")
mock_mongodb.update_avis_critique = Mock(return_value=True)
mock_mongodb.get_avis_critique_by_id.return_value = None
mock_mongodb.avis_critiques_collection = Mock()
mock_mongodb.livres_collection = Mock()
service.mongodb_service = mock_mongodb
service._mock_mongodb = mock_mongodb
return service
def patch_cache_service(is_already_corrected=False):
"""Returns patches for livres_auteurs_cache_service singleton."""
from unittest.mock import patch as mock_patch
return (
mock_patch("back_office_lmelp.services.livres_auteurs_cache_service.livres_auteurs_cache_service.mark_as_processed", return_value=True),
mock_patch("back_office_lmelp.services.livres_auteurs_cache_service.livres_auteurs_cache_service.is_summary_corrected", return_value=is_already_corrected),
mock_patch("back_office_lmelp.services.livres_auteurs_cache_service.livres_auteurs_cache_service.mark_summary_corrected", return_value=True),
)
class TestAvisCritiqueSummaryCorrection:
def test_should_update_summary_with_correction(self):
# Arrange
service = create_mocked_service()
service._mock_mongodb.get_avis_critique_by_id.return_value = {
"_id": "507f...",
"summary": "Original | Alain Mabancou | Book |"
}
book_data = {
"cache_id": "507f...",
"avis_critique_id": "507f...",
"auteur": "Alain Mabancou",
"titre": "Book",
"user_validated_author": "Alain Mabanckou", # Correction
}
# Act - Patch singleton before calling method with local import
patches = patch_cache_service()
with patches[0], patches[1], patches[2]:
service.handle_book_validation(book_data)
# Assert
assert service._mock_mongodb.update_avis_critique.called
updates = service._mock_mongodb.update_avis_critique.call_args[0][1]
assert "Alain Mabanckou" in updates["summary"]
def test_idempotence_skip_if_already_corrected(self):
# Arrange
service = create_mocked_service()
book_data = {...}
# Act - Use is_already_corrected=True for idempotence test
patches = patch_cache_service(is_already_corrected=True)
with patches[0], patches[1], patches[2]:
service.handle_book_validation(book_data)
# Assert - Should NOT update (already corrected)
service._mock_mongodb.update_avis_critique.assert_not_called()
Common Pitfalls to Avoid¶
❌ Pitfall 1: Patching the Wrong Path¶
# Wrong: Patches module namespace, not singleton instance
with patch("back_office_lmelp.services.collections_management_service.livres_auteurs_cache_service"):
pass # Local import still gets real singleton
❌ Pitfall 2: Patching Too Late¶
# Wrong: Service already called before patch applied
service.handle_book_validation(book_data) # Uses real singleton
with patch("...livres_auteurs_cache_service.mark_as_processed"):
pass # Too late!
✅ Correct Timing¶
patches = patch_cache_service()
with patches[0], patches[1], patches[2]:
service.handle_book_validation(book_data) # Patches active during call
Architecture Implications¶
Why Singletons with Local Imports Are Problematic¶
- Testability: Harder to mock, requires instance-level patching
- Import Cycles: Local imports often indicate circular dependencies
- Predictability: Harder to reason about when dependencies are resolved
Better Alternative: Dependency Injection¶
# ✅ Better: Dependency injection
class CollectionsManagementService:
def __init__(self, cache_service=None):
self.cache_service = cache_service or livres_auteurs_cache_service
def handle_book_validation(self, book_data):
# No local import, use injected dependency
self.cache_service.mark_as_processed(cache_id, author_id, book_id)
# Test with easy mocking
def test_with_di():
mock_cache = Mock()
service = CollectionsManagementService(cache_service=mock_cache)
service.handle_book_validation(book_data)
mock_cache.mark_as_processed.assert_called_once()
When to Use Local Imports (Acceptable Cases)¶
- Breaking circular import cycles (temporary fix)
- Lazy loading for performance (rare)
- Type checking only (
if TYPE_CHECKING:)
Best Practices Summary¶
- Name helpers clearly:
create_mocked_service()notget_service() - Document why: Add docstring explaining pattern avoids fixture issues
- Expose mocks: Store mock in
service._mock_xyzfor assertions - Configure defaults: Set safe default return values
- Per-test customization: Override specific behaviors in each test
- Prefer dependency injection: Refactor to DI when possible for easier testing
Test Results¶
Using these patterns, Issue #67 achieved:
- 12/12 tests passing (100% success rate)
- 94% coverage of summary_updater.py
- 45% coverage of collections_management_service.py (improved from 29%)
- Zero "Connexion MongoDB non établie" errors
References¶
- Issue #67: Mise à jour du summary après validation
- CLAUDE.md: Lines 385-819 (full technical documentation for Claude Code)
- Test File:
tests/test_avis_critique_summary_correction.py - Discovered: After 12 failed test attempts with standard mocking approaches
Maintenance¶
When encountering "Connexion MongoDB non établie" errors in tests:
- Check if service uses local imports inside methods
- Check if imported object is a singleton instance
- Apply Pattern 2 (singleton instance patching)
- Use helper functions from this document
- Verify patches applied before method call
This documentation serves as a reference for all future test development involving complex service dependencies and singleton patterns.