Skip to content

Fake Helpers: Documentation and Testing Guide Updates #354

@mikelane

Description

@mikelane

Summary

Document the new testing utilities and update the testing guide to showcase how to use them effectively.

Problem Statement

Testing utilities exist but developers need:

  • Understanding of when to use each utility
  • Examples of extending base classes
  • Patterns for common testing scenarios

Acceptance Criteria

  • docs/TESTING_GUIDE.md updated with fake helpers section
  • API reference for all dioxide.testing utilities
  • Example: Custom repository fake with additional query methods
  • Example: Testing time-dependent logic with FakeClock
  • Example: Verifying event publication with FakeEventBus
  • Example: Testing external API integration with FakeHttpClient
  • All code examples tested

Documentation Content

Testing Guide Updates

## Built-in Fake Helpers

dioxide provides base classes for common fake implementations in `dioxide.testing`.

### InMemoryRepository

Generic base class for fake repositories:

\`\`\`python
from dioxide.testing import InMemoryRepository
from dioxide import adapter, Profile

@adapter.for_(UserRepository, profile=Profile.TEST)
class FakeUserRepository(InMemoryRepository[User, int]):
    def get_id(self, user: User) -> int:
        return user.id
    
    def set_id(self, user: User, id: int) -> User:
        user.id = id
        return user
    
    # Add custom query methods as needed
    def find_by_email(self, email: str) -> User | None:
        matches = self.find_by(lambda u: u.email == email)
        return matches[0] if matches else None

# Usage in tests
async def test_user_registration():
    repo = container.resolve(UserRepository)
    repo.seed(User(id=1, name="Existing", email="exists@example.com"))
    
    service = container.resolve(UserService)
    
    # Should fail - email already exists
    with pytest.raises(DuplicateEmailError):
        await service.register("exists@example.com", "New User")
\`\`\`

### FakeClock

Controllable time for testing time-dependent logic:

\`\`\`python
from dioxide.testing import FakeClock
from dioxide import adapter, Profile

@adapter.for_(Clock, profile=Profile.TEST)
class TestClock(FakeClock):
    pass

# Usage in tests
async def test_subscription_expiry():
    clock = container.resolve(Clock)
    clock.set_time(datetime(2024, 1, 1))
    
    service = container.resolve(SubscriptionService)
    sub = await service.create_subscription(user_id=1, days=30)
    
    # Still valid
    assert await service.is_active(sub.id) is True
    
    # Advance past expiry
    clock.advance(days=31)
    
    # Now expired
    assert await service.is_active(sub.id) is False
\`\`\`

### FakeEventBus

Capture and verify published events:

\`\`\`python
from dioxide.testing import FakeEventBus
from dioxide import adapter, Profile

@adapter.for_(EventBus, profile=Profile.TEST)
class TestEventBus(FakeEventBus[DomainEvent]):
    pass

# Usage in tests
async def test_user_created_event_published():
    bus = container.resolve(EventBus)
    service = container.resolve(UserService)
    
    await service.register("alice@example.com", "Alice")
    
    # Verify event was published
    assert bus.has_event(UserCreated)
    
    # Check event details
    event = bus.events_of_type(UserCreated)[0]
    assert event.email == "alice@example.com"
\`\`\`

### FakeHttpClient

Record and stub HTTP requests:

\`\`\`python
from dioxide.testing import FakeHttpClient
from dioxide import adapter, Profile

@adapter.for_(HttpClient, profile=Profile.TEST)
class TestHttpClient(FakeHttpClient):
    pass

# Usage in tests
async def test_fetches_user_from_api():
    client = container.resolve(HttpClient)
    client.stub_response(
        "GET", 
        "https://api.example.com/users/123",
        {"id": 123, "name": "Alice"},
    )
    
    service = container.resolve(ExternalUserService)
    user = await service.fetch_user(123)
    
    assert user.name == "Alice"
    assert client.was_called("GET", "https://api.example.com/users/123")
\`\`\`

## When to Use Built-in Fakes vs Custom

| Scenario | Use |
|----------|-----|
| Standard CRUD repository | `InMemoryRepository` |
| Time-dependent logic | `FakeClock` |
| Event verification | `FakeEventBus` |
| External HTTP APIs | `FakeHttpClient` |
| Complex domain logic | Custom fake |
| Stateful protocols | Custom fake |

Dependencies

Parent Epic

Part of #336 (Production-Ready Developer Experience Improvements)

Definition of Ready

  • Implementation story completed
  • Acceptance criteria are testable
  • Documentation outline complete

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions