Troubleshooting¶
Common pitfalls and solutions when testing with dioxide.
Common Pitfalls¶
Pitfall 1: Fakes That Are Too Complex¶
Problem: Fake becomes harder to understand than real implementation.
# BAD: Fake is too complex
class FakeUserRepository:
def __init__(self):
self.users = {}
self.transaction_log = []
self.locks = {}
async def create(self, name: str, email: str) -> dict:
# Complex transaction simulation
lock_id = self._acquire_lock()
try:
if email in [u["email"] for u in self.users.values()]:
raise DuplicateEmailError()
# ... 50 lines of complex logic
finally:
self._release_lock(lock_id)
# GOOD: Fake is simple
class FakeUserRepository:
def __init__(self):
self.users = {}
async def create(self, name: str, email: str) -> dict:
user = {"id": len(self.users) + 1, "name": name, "email": email}
self.users[user["id"]] = user
return user
Solution: Keep fakes simple. If you need to test complex behavior (transactions, locks), write integration tests with real implementation.
Pitfall 2: Not Resetting Fakes Between Tests¶
Problem: State leaks between tests cause flaky failures.
# BAD: Tests affect each other
async def test_first(fake_email, service):
await service.register("alice@example.com")
assert len(fake_email.sent_emails) == 1
async def test_second(fake_email, service):
# FAILS! sent_emails still has 1 email from previous test
assert len(fake_email.sent_emails) == 0 # Flaky!
# GOOD: Reset between tests
@pytest.fixture
def fake_email(container):
adapter = container.resolve(EmailPort)
yield adapter
adapter.clear() # Clean state
# Or use fresh container
@pytest.fixture
def container():
c = Container()
c.scan(profile=Profile.TEST)
return c # Fresh container = isolated tests
Solution: Either reset fakes explicitly or use fresh container per test.
Pitfall 3: Using Fakes for Pure Functions¶
Problem: Faking things that don’t need faking.
# BAD: Unnecessary fake
def calculate_discount(price: float, percent: float) -> float:
return price * (percent / 100)
# Don't fake this! It's a pure function
class FakeDiscountCalculator:
def calculate(self, price: float, percent: float) -> float:
return price * (percent / 100)
# GOOD: Test directly
def test_discount():
result = calculate_discount(100.0, 10.0)
assert result == 10.0
Solution: Only fake at architectural boundaries (ports). Pure functions don’t need fakes.
Pitfall 4: Adding Business Logic to Fakes¶
Problem: Fakes become a second implementation to maintain.
# BAD: Fake has business logic
class FakeUserRepository:
async def create(self, name: str, email: str) -> dict:
# Business rule duplicated in fake!
if len(name) < 3:
raise ValidationError("Name too short")
# ...
# GOOD: Fake is dumb, validation is in service
@service
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
async def register(self, name: str, email: str):
# Business rule in service
if len(name) < 3:
raise ValidationError("Name too short")
return await self.repo.create(name, email)
class FakeUserRepository:
async def create(self, name: str, email: str) -> dict:
# Dumb storage
user = {"id": 1, "name": name, "email": email}
self.users[1] = user
return user
Solution: Keep business logic in services, not fakes. Fakes should be dumb storage/transport.
Pitfall 5: Mixing Fakes and Mocks¶
Problem: Inconsistent testing strategy.
# BAD: Mixing fakes and mocks
async def test_mixed(container):
# Use dioxide fake for email
service = container.resolve(UserService)
# But use mock for database (inconsistent!)
with patch('app.database.save') as mock_save:
await service.register("Alice", "alice@example.com")
mock_save.assert_called_once()
# GOOD: Consistent - all fakes
async def test_consistent(container):
service = container.resolve(UserService)
fake_users = container.resolve(UserRepository)
fake_email = container.resolve(EmailPort)
await service.register("Alice", "alice@example.com")
assert len(fake_users.users) == 1
assert len(fake_email.sent_emails) == 1
Solution: Be consistent. Either use fakes everywhere or mocks everywhere (prefer fakes).
Frequently Asked Questions¶
When should I use fakes vs real implementations?¶
Use fakes for:
Unit tests (fast, isolated)
Development environment (no real services needed)
Demos and documentation
CI/CD (fast pipeline)
Use real implementations for:
Integration tests (verify real behavior)
Staging environment (realistic testing)
Production (obviously)
Rule of thumb: Use fakes unless you specifically need to test integration with real services.
How do I test error cases with fakes?¶
Make fakes configurable to fail:
class FakeEmailAdapter:
def __init__(self):
self.should_fail = False
self.failure_reason = "Network error"
async def send(self, to: str, subject: str, body: str) -> None:
if self.should_fail:
raise EmailError(self.failure_reason)
# ... normal behavior
# In test
async def test_handles_email_failure(fake_email, service):
fake_email.should_fail = True
fake_email.failure_reason = "SMTP timeout"
with pytest.raises(EmailError):
await service.register("alice@example.com")
Can I use fakes with existing test frameworks?¶
Yes! dioxide fakes work with any testing framework:
# pytest
async def test_with_pytest(container):
service = container.resolve(UserService)
# ...
# unittest
class TestUserService(unittest.TestCase):
def setUp(self):
self.container = Container()
self.container.scan(profile=Profile.TEST)
async def test_registration(self):
service = self.container.resolve(UserService)
# ...
# Robot Framework, behave, etc.
# Just create container with TEST profile and use it
What about stubbing third-party APIs?¶
For third-party APIs that you don’t control, create a port and two adapters:
from typing import Protocol
from dioxide import adapter, Profile
import httpx
# Port (your interface)
class WeatherPort(Protocol):
async def get_temperature(self, city: str) -> float: ...
# Production adapter (calls real API)
@adapter.for_(WeatherPort, profile=Profile.PRODUCTION)
class OpenWeatherAdapter:
async def get_temperature(self, city: str) -> float:
# Real API call
response = await httpx.get(f"https://api.openweather.org/...")
return response.json()["temperature"]
# Test fake (returns predictable data)
@adapter.for_(WeatherPort, profile=Profile.TEST)
class FakeWeatherAdapter:
def __init__(self):
self.temperatures = {"Seattle": 15.5, "Miami": 28.0}
async def get_temperature(self, city: str) -> float:
return self.temperatures.get(city, 20.0)
Should fakes implement all protocol methods?¶
Yes! Fakes should implement the complete protocol. This ensures:
Type checkers validate the fake
Tests exercise the full interface
Changes to protocol affect fakes (you’ll know)
# Port
class UserRepository(Protocol):
async def find_by_id(self, user_id: int) -> dict | None: ...
async def create(self, name: str, email: str) -> dict: ...
async def update(self, user: dict) -> None: ...
async def delete(self, user_id: int) -> None: ...
# Fake MUST implement all methods
class FakeUserRepository:
async def find_by_id(self, user_id: int) -> dict | None:
# ...
async def create(self, name: str, email: str) -> dict:
# ...
async def update(self, user: dict) -> None:
# ...
async def delete(self, user_id: int) -> None:
# ...
If some methods aren’t needed in tests yet, implement them as no-ops:
async def delete(self, user_id: int) -> None:
# Not used in tests yet, but required by protocol
self.users.pop(user_id, None)
How do I handle fakes that need cleanup?¶
Use test fixtures with cleanup:
import tempfile
import shutil
import pytest
from dioxide import Container
# Fake that creates temp files
class FakeFileStorage:
def __init__(self):
self.temp_dir = tempfile.mkdtemp()
def cleanup(self):
shutil.rmtree(self.temp_dir)
# Fixture with cleanup
@pytest.fixture
def fake_storage(container: Container):
storage = container.resolve(FileStorage)
yield storage
storage.cleanup()
Or use lifecycle (@lifecycle) if the fake needs async cleanup:
import tempfile
import shutil
from dioxide import adapter, lifecycle, Profile
@adapter.for_(FileStorage, profile=Profile.TEST)
@lifecycle
class FakeFileStorage:
async def initialize(self) -> None:
self.temp_dir = tempfile.mkdtemp()
async def dispose(self) -> None:
shutil.rmtree(self.temp_dir)
How do I test code that depends on the current time?¶
Use a fake clock:
from datetime import datetime, timedelta, UTC
from typing import Protocol
from dioxide import adapter, Profile
# Port
class Clock(Protocol):
def now(self) -> datetime: ...
# Fake
@adapter.for_(Clock, profile=Profile.TEST)
class FakeClock:
def __init__(self):
self._now = datetime(2024, 1, 1, tzinfo=UTC)
def now(self) -> datetime:
return self._now
def set_time(self, dt: datetime) -> None:
self._now = dt
def advance(self, **kwargs) -> None:
self._now += timedelta(**kwargs)
# Test
async def test_time_dependent(fake_clock, service):
fake_clock.set_time(datetime(2024, 1, 1, tzinfo=UTC))
# ... test at specific time
fake_clock.advance(days=30)
# ... test 30 days later
This eliminates flaky tests from time-dependent logic.
Error Messages¶
Missing Adapter for Port¶
Error: NoAdapterError: No adapter registered for port 'EmailPort' in profile 'test'
Cause: No adapter is registered for the port in the active profile.
Solution: Create an adapter for the port with the correct profile:
@adapter.for_(EmailPort, profile=Profile.TEST)
class FakeEmailAdapter:
async def send(self, to: str, subject: str, body: str) -> None:
self.sent_emails.append({"to": to, "subject": subject, "body": body})
Circular Dependency¶
Error: CircularDependencyError: Circular dependency detected: A -> B -> A
Cause: Two or more services depend on each other in a cycle.
Solution: Refactor to break the cycle:
Extract shared logic into a third service
Use dependency inversion (depend on port, not concrete service)
Use events for loose coupling
Unresolvable Dependency¶
Error: UnresolvableDependencyError: Cannot resolve 'Config' - no adapter or service registered
Cause: A dependency type is not registered in the container.
Solution: Register the dependency:
@service
class Config:
"""Application configuration."""
pass
Or for adapters:
@adapter.for_(ConfigPort, profile=Profile.PRODUCTION)
class EnvConfig:
pass
See also
Testing Philosophy: Fakes Over Mocks - Understanding the testing philosophy
Testing Patterns - Common fake implementation patterns
Test Fixtures - Container fixtures for pytest