Test Scopes¶
Test scopes handle testing of scoped services, ensuring proper lifecycle management and isolation in tests.
🎯 Understanding Scopes in Testing¶
Scope Lifecycle in Tests¶
from injectq import scoped, singleton, transient
# Scoped service - New instance per scope
@scoped
class RequestContext:
def __init__(self):
self.request_id = str(uuid.uuid4())
self.user_id = None
self.metadata = {}
# Singleton service - Same instance for entire container
@singleton
class ApplicationConfig:
def __init__(self):
self.settings = {}
self.initialized_at = time.time()
# Transient service - New instance every time
@transient
class DataProcessor:
def __init__(self):
self.created_at = time.time()
def test_scope_lifecycle(container):
"""Test how different scopes behave in tests."""
# Scoped service
context1 = container.get(RequestContext)
context2 = container.get(RequestContext)
# In same scope: same instance
assert context1 is context2
assert context1.request_id == context2.request_id
# Singleton service
config1 = container.get(ApplicationConfig)
config2 = container.get(ApplicationConfig)
# Always same instance
assert config1 is config2
assert config1.initialized_at == config2.initialized_at
# Transient service
processor1 = container.get(DataProcessor)
processor2 = container.get(DataProcessor)
# Always different instances
assert processor1 is not processor2
assert processor1.created_at != processor2.created_at
🔧 Testing Scoped Services¶
Request Scope Testing¶
@scoped
class RequestContext:
def __init__(self):
self.request_id = str(uuid.uuid4())
self.user = None
self.start_time = time.time()
self.metadata = {}
def set_user(self, user: User):
self.user = user
def add_metadata(self, key: str, value: Any):
self.metadata[key] = value
def test_request_scoped_service(container):
"""Test request-scoped service behavior."""
# Get service in current scope
context1 = container.get(RequestContext)
context1.set_user(User(id=1, name="John"))
context1.add_metadata("source", "test")
# Same scope: same instance
context2 = container.get(RequestContext)
assert context1 is context2
assert context2.user.name == "John"
assert context2.metadata["source"] == "test"
# Verify scope isolation
assert context1.request_id == context2.request_id
Scope Boundary Testing¶
def test_scope_boundaries(container):
"""Test behavior at scope boundaries."""
# Start with one scope
context1 = container.get(RequestContext)
context1.set_user(User(id=1, name="John"))
# Simulate scope end (in real app, this would be automatic)
container.end_scope()
# New scope: new instance
context2 = container.get(RequestContext)
assert context1 is not context2
assert context1.request_id != context2.request_id
assert context2.user is None # Fresh instance
# Set data in new scope
context2.set_user(User(id=2, name="Jane"))
assert context2.user.name == "Jane"
# Original scope unchanged
assert context1.user.name == "John"
Nested Scope Testing¶
def test_nested_scopes(container):
"""Test nested scope behavior."""
# Outer scope
outer_context = container.get(RequestContext)
outer_context.set_user(User(id=1, name="John"))
outer_context.add_metadata("level", "outer")
# Start nested scope
container.begin_nested_scope()
# Inner scope
inner_context = container.get(RequestContext)
assert inner_context is not outer_context # Different instances
assert inner_context.request_id != outer_context.request_id
# Inner scope inherits some data (depending on implementation)
inner_context.set_user(User(id=2, name="Jane"))
inner_context.add_metadata("level", "inner")
# Verify isolation
assert outer_context.user.name == "John"
assert inner_context.user.name == "Jane"
# End nested scope
container.end_scope()
# Back to outer scope
current_context = container.get(RequestContext)
assert current_context is outer_context
assert current_context.user.name == "John"
assert current_context.metadata["level"] == "outer"
🎨 Scope Testing Patterns¶
Scope-Aware Test Fixtures¶
@pytest.fixture
def scoped_container():
"""Container with proper scope management."""
container = TestContainer()
# Bind scoped services
container.bind(RequestContext, RequestContext())
container.bind(UserSession, UserSession())
# Start test scope
container.begin_scope()
try:
yield container
finally:
# Clean up scope
container.end_scope()
def test_with_scoped_fixture(scoped_container):
"""Test using scoped fixture."""
# Services are in test scope
context = scoped_container.get(RequestContext)
session = scoped_container.get(UserSession)
# Use services
context.set_user(User(id=1, name="Test User"))
session.set_data("key", "value")
# Verify in same scope
assert context.user.name == "Test User"
assert session.get_data("key") == "value"
Scope Isolation Testing¶
def test_scope_isolation(container):
"""Test that scopes are properly isolated."""
users = []
# Create multiple "requests" (scopes)
for i in range(3):
# Start new scope for each "request"
container.begin_scope()
context = container.get(RequestContext)
context.set_user(User(id=i+1, name=f"User {i+1}"))
# Simulate request processing
user = context.user
users.append(user)
# End scope
container.end_scope()
# Verify isolation
assert len(users) == 3
assert users[0].name == "User 1"
assert users[1].name == "User 2"
assert users[2].name == "User 3"
# Verify different request IDs
request_ids = [u.id for u in users]
assert len(set(request_ids)) == 3 # All unique
Scope Lifecycle Testing¶
@scoped
class ScopedService:
def __init__(self):
self.created_at = time.time()
self.operations = []
self.disposed = False
def do_operation(self, name: str):
self.operations.append({
"name": name,
"timestamp": time.time()
})
def dispose(self):
self.disposed = True
def test_scope_lifecycle(container):
"""Test scoped service lifecycle."""
# Start scope
container.begin_scope()
# Get service
service1 = container.get(ScopedService)
service1.do_operation("init")
# Same scope: same instance
service2 = container.get(ScopedService)
assert service1 is service2
assert len(service1.operations) == 1
# Do more operations
service2.do_operation("process")
assert len(service1.operations) == 2
# End scope
container.end_scope()
# Verify disposal
assert service1.disposed is True
# New scope: new instance
container.begin_scope()
service3 = container.get(ScopedService)
assert service3 is not service1
assert service3.disposed is False
assert len(service3.operations) == 0
container.end_scope()
🧪 Advanced Scope Testing¶
Concurrent Scope Testing¶
import asyncio
async def test_concurrent_scopes(container):
"""Test scopes in concurrent scenarios."""
results = []
async def process_request(request_id: int):
# Each "request" gets its own scope
container.begin_scope()
try:
context = container.get(RequestContext)
context.set_user(User(id=request_id, name=f"User {request_id}"))
# Simulate async processing
await asyncio.sleep(0.01)
# Verify isolation
result = {
"request_id": request_id,
"user_name": context.user.name,
"context_id": id(context)
}
results.append(result)
finally:
container.end_scope()
# Process multiple concurrent requests
tasks = [process_request(i) for i in range(5)]
await asyncio.gather(*tasks)
# Verify all requests were isolated
assert len(results) == 5
# All should have different context instances
context_ids = [r["context_id"] for r in results]
assert len(set(context_ids)) == 5
# Each should have correct user
for i, result in enumerate(results):
assert result["user_name"] == f"User {i}"
@pytest.mark.asyncio
async def test_async_scope_isolation(container):
"""Test async scope isolation."""
await test_concurrent_scopes(container)
Scope Context Manager Testing¶
class ScopeContext:
"""Context manager for scope testing."""
def __init__(self, container):
self.container = container
def __enter__(self):
self.container.begin_scope()
return self.container
def __exit__(self, exc_type, exc_val, exc_tb):
self.container.end_scope()
def test_scope_context_manager(container):
"""Test scope management with context manager."""
results = []
for i in range(3):
with ScopeContext(container) as scoped_container:
context = scoped_container.get(RequestContext)
context.set_user(User(id=i+1, name=f"User {i+1}"))
results.append({
"user_name": context.user.name,
"context_id": id(context)
})
# Verify results
assert len(results) == 3
assert results[0]["user_name"] == "User 1"
assert results[1]["user_name"] == "User 2"
assert results[2]["user_name"] == "User 3"
# Verify different contexts
context_ids = [r["context_id"] for r in results]
assert len(set(context_ids)) == 3
Scope Inheritance Testing¶
@scoped
class ParentScope:
def __init__(self):
self.data = {"level": "parent"}
@scoped
class ChildScope:
def __init__(self, parent: ParentScope):
self.parent = parent
self.data = {"level": "child"}
def test_scope_inheritance(container):
"""Test scope inheritance behavior."""
# Parent scope
container.begin_scope()
parent = container.get(ParentScope)
parent.data["parent_value"] = "inherited"
# Child scope
container.begin_nested_scope()
child = container.get(ChildScope)
assert child.parent is parent # Should inherit parent
assert child.parent.data["parent_value"] == "inherited"
# Child can have its own data
child.data["child_value"] = "unique"
# Parent unchanged
assert parent.data["level"] == "parent"
assert "child_value" not in parent.data
# End child scope
container.end_scope()
# Back to parent scope
current_parent = container.get(ParentScope)
assert current_parent is parent
assert current_parent.data["parent_value"] == "inherited"
# End parent scope
container.end_scope()
🚨 Scope Testing Challenges¶
Scope Leakage¶
# ❌ Bad: Scope leakage between tests
def test_scope_leakage_problem(container):
# Test 1
context1 = container.get(RequestContext)
context1.set_user(User(id=1, name="User 1"))
# Test runs, but scope not cleaned up
# Next test will see User 1's data!
def test_scope_leakage_problem2(container):
# Test 2 - sees data from test 1!
context2 = container.get(RequestContext)
assert context2.user.name == "User 1" # Unexpected!
# ✅ Good: Proper scope cleanup
@pytest.fixture
def clean_scoped_container():
container = TestContainer()
container.begin_scope()
try:
yield container
finally:
container.end_scope()
def test_with_proper_cleanup(clean_scoped_container):
context = clean_scoped_container.get(RequestContext)
context.set_user(User(id=1, name="User 1"))
assert context.user.name == "User 1"
def test_isolated_with_proper_cleanup(clean_scoped_container):
context = clean_scoped_container.get(RequestContext)
# Fresh scope, no data from previous test
assert context.user is None
Async Scope Issues¶
# ❌ Bad: Async scope issues
async def test_async_scope_problem(container):
# Start scope in async function
container.begin_scope()
context = container.get(RequestContext)
context.set_user(User(id=1, name="User 1"))
# Async operation
await some_async_operation()
# Forget to end scope - memory leak!
# container.end_scope() # Missing!
# ✅ Good: Proper async scope management
@pytest.fixture
async def async_scoped_container():
container = TestContainer()
container.begin_scope()
try:
yield container
finally:
container.end_scope()
async def test_proper_async_scope(async_scoped_container):
context = async_scoped_container.get(RequestContext)
context.set_user(User(id=1, name="User 1"))
await some_async_operation()
# Scope automatically cleaned up by fixture
Threading Scope Issues¶
import threading
# ❌ Bad: Threading scope issues
def test_threading_scope_problem(container):
results = []
def worker_thread(thread_id):
# Each thread should have its own scope
container.begin_scope() # Wrong! Shared container
context = container.get(RequestContext)
context.set_user(User(id=thread_id, name=f"Thread {thread_id}"))
results.append(context.user.name)
# container.end_scope() # Missing cleanup
threads = [threading.Thread(target=worker_thread, args=(i,)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
# Results unpredictable due to shared scopes
# ✅ Good: Thread-local scopes
def test_threading_scope_solution():
results = []
def worker_thread(thread_id):
# Each thread gets its own container
container = TestContainer()
container.begin_scope()
try:
context = container.get(RequestContext)
context.set_user(User(id=thread_id, name=f"Thread {thread_id}"))
results.append((thread_id, context.user.name))
finally:
container.end_scope()
threads = [threading.Thread(target=worker_thread, args=(i,)) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
# Each thread has isolated scope
assert len(results) == 3
for thread_id, user_name in results:
assert user_name == f"Thread {thread_id}"
📊 Scope Testing Metrics¶
Scope Usage Tracking¶
class ScopeTrackingContainer(TestContainer):
"""Container that tracks scope usage for testing."""
def __init__(self):
super().__init__()
self.scope_stack = []
self.scope_metrics = {
"created_scopes": 0,
"active_scopes": 0,
"max_nested_depth": 0,
"scope_lifetimes": []
}
def begin_scope(self):
super().begin_scope()
self.scope_stack.append(time.time())
self.scope_metrics["created_scopes"] += 1
self.scope_metrics["active_scopes"] += 1
self.scope_metrics["max_nested_depth"] = max(
self.scope_metrics["max_nested_depth"],
len(self.scope_stack)
)
def end_scope(self):
if self.scope_stack:
start_time = self.scope_stack.pop()
lifetime = time.time() - start_time
self.scope_metrics["scope_lifetimes"].append(lifetime)
self.scope_metrics["active_scopes"] -= 1
super().end_scope()
def get_scope_metrics(self):
return self.scope_metrics.copy()
def test_scope_metrics_tracking():
"""Test scope usage tracking."""
container = ScopeTrackingContainer()
# Create some scopes
container.begin_scope()
time.sleep(0.01) # Simulate work
container.end_scope()
container.begin_scope()
container.begin_nested_scope()
time.sleep(0.005)
container.end_scope()
time.sleep(0.01)
container.end_scope()
# Check metrics
metrics = container.get_scope_metrics()
assert metrics["created_scopes"] == 3 # 2 regular + 1 nested
assert metrics["max_nested_depth"] == 2
assert len(metrics["scope_lifetimes"]) == 3
assert all(lifetime > 0 for lifetime in metrics["scope_lifetimes"])
Scope Performance Testing¶
def test_scope_performance(container):
"""Test scope creation and destruction performance."""
import time
# Test scope creation performance
creation_times = []
for _ in range(100):
start = time.time()
container.begin_scope()
creation_times.append(time.time() - start)
container.end_scope()
avg_creation_time = sum(creation_times) / len(creation_times)
# Should be very fast
assert avg_creation_time < 0.001 # Less than 1ms
# Test nested scope performance
nested_times = []
for _ in range(50):
container.begin_scope()
start = time.time()
for _ in range(5): # 5 levels of nesting
container.begin_nested_scope()
nested_creation_time = time.time() - start
nested_times.append(nested_creation_time)
# Clean up nested scopes
for _ in range(5):
container.end_scope()
container.end_scope()
avg_nested_time = sum(nested_times) / len(nested_times)
assert avg_nested_time < 0.005 # Less than 5ms for 5 nested scopes
✅ Scope Testing Best Practices¶
1. Use Proper Scope Management¶
# ✅ Good: Fixture with automatic cleanup
@pytest.fixture
def scoped_container():
container = TestContainer()
container.begin_scope()
try:
yield container
finally:
container.end_scope()
def test_with_proper_scope_management(scoped_container):
# Scope automatically managed
context = scoped_container.get(RequestContext)
# Test logic
pass # Scope cleaned up automatically
# ❌ Bad: Manual scope management
def test_manual_scope_management(container):
container.begin_scope()
try:
# Test logic
context = container.get(RequestContext)
# ...
finally:
container.end_scope() # Easy to forget
2. Test Scope Isolation¶
# ✅ Good: Test scope isolation
def test_scope_isolation(scoped_container):
# Each test gets fresh scope
context1 = scoped_container.get(RequestContext)
context1.set_user(User(id=1, name="User 1"))
# Modify in this test
assert context1.user.name == "User 1"
def test_scope_isolation2(scoped_container):
# Different test, fresh scope
context2 = scoped_container.get(RequestContext)
# No data from previous test
assert context2.user is None
# ❌ Bad: Shared scope between tests
def test_shared_scope_problem(container):
# No scope management - shared state
context = container.get(RequestContext)
# Tests interfere with each other
3. Test Scope Boundaries¶
# ✅ Good: Test scope boundaries
def test_scope_boundaries(container):
# Test scope start
container.begin_scope()
context = container.get(RequestContext)
context.set_user(User(id=1, name="Test"))
# Verify in scope
assert context.user.name == "Test"
# Test scope end
container.end_scope()
# Verify after scope end
container.begin_scope()
new_context = container.get(RequestContext)
assert new_context.user is None # Fresh scope
container.end_scope()
# ✅ Test nested scopes
def test_nested_scope_behavior(container):
container.begin_scope()
outer = container.get(RequestContext)
outer.set_user(User(id=1, name="Outer"))
container.begin_nested_scope()
inner = container.get(RequestContext)
# Different instances
assert inner is not outer
assert inner.user is None # No inheritance by default
container.end_scope() # End nested
container.end_scope() # End outer
4. Handle Async Scopes Properly¶
# ✅ Good: Async scope management
@pytest.fixture
async def async_scoped_container():
container = TestContainer()
container.begin_scope()
try:
yield container
finally:
container.end_scope()
@pytest.mark.asyncio
async def test_async_scopes(async_scoped_container):
context = async_scoped_container.get(RequestContext)
context.set_user(User(id=1, name="Async User"))
# Async operations
await asyncio.sleep(0.01)
# Verify scope maintained
assert context.user.name == "Async User"
# ✅ Test concurrent scopes
@pytest.mark.asyncio
async def test_concurrent_scopes():
async def scoped_operation(container_factory, operation_id):
container = container_factory()
container.begin_scope()
try:
context = container.get(RequestContext)
context.set_user(User(id=operation_id, name=f"User {operation_id}"))
await asyncio.sleep(0.01) # Simulate work
return context.user.name
finally:
container.end_scope()
# Run concurrent operations
tasks = [scoped_operation(TestContainer, i) for i in range(5)]
results = await asyncio.gather(*tasks)
# Verify isolation
assert len(set(results)) == 5 # All different
assert all(f"User {i}" in results for i in range(5))
🎯 Summary¶
Scope testing ensures proper lifecycle management and isolation:
- Scope lifecycle - Test creation, usage, and cleanup
- Scope isolation - Ensure scopes don't interfere
- Nested scopes - Test hierarchical scope behavior
- Async scopes - Handle concurrent and async scenarios
- Scope boundaries - Test transitions between scopes
Key principles: - Use fixtures for automatic scope management - Test scope isolation between tests - Verify proper cleanup and disposal - Handle async and concurrent scenarios - Monitor scope performance and usage
Best practices: - Use context managers for scope management - Ensure proper test isolation - Test both success and failure scenarios - Monitor scope metrics and performance - Handle threading and async scope issues - Test scope inheritance and nesting
Common patterns: - Scope-aware test fixtures - Scope context managers - Concurrent scope testing - Scope usage tracking - Performance monitoring
Ready to explore testing best practices?