Testing Utilities¶
The framework provides testing utilities for building domain objects, inspecting events, and creating stub containers for integration testing.
Imports¶
from aod.testing import build, events_of, assert_event_emitted, assert_no_events, check_invariant
from aod.testing import FakeDomain
from aod.testing.doubles import (
SpyLogger,
SpyEventBus,
port_stub,
spy_session,
spy_adapter_container,
spy_command_handler,
spy_query_handler,
spy_async_command_handler,
spy_async_query_handler,
)
Helper Functions¶
build¶
Create an instance of a domain class skipping validation.
Parameters¶
| Parameter | Type | Description |
|---|---|---|
cls |
type[T] |
The class to instantiate. Must be an Entity, RootEntity, ValueObject, or any domain object subclass. |
**kwargs |
Any |
Field values to set on the instance. |
Returns¶
T — An instance of cls without validation.
events_of¶
Extract all events emitted by a domain object via its _event_emitter.
Parameters¶
| Parameter | Type | Description |
|---|---|---|
obj |
object |
The domain object (Entity, ValueObject, Service, etc.). |
Returns¶
list[Event] — All events emitted by the object since construction or last clear.
assert_event_emitted¶
Assert that a specific event type was emitted with matching field values.
Parameters¶
| Parameter | Type | Description |
|---|---|---|
events |
Sequence[Event] |
The list of events to search (from events_of()). |
event_type |
type[Event] |
The expected event class. |
**attrs |
Any |
Field values that the event must match. |
Returns¶
Event — The matching event instance.
assert_no_events¶
Assert that no events were emitted.
check_invariant¶
def check_invariant(cls: type, invariant_name: str, data: dict[str, Any] | None = None, **kwargs: Any) -> None
Run a single invariant validator against an instance created via build().
Container Testing with spy_adapter_container¶
The recommended approach for testing use cases is to create a spy version of your container. This replaces sessions and ports with stubs that record calls and let you configure return values.
from aod.infrastructure import AdapterContainer
from aod.testing.doubles import spy_adapter_container
container = spy_adapter_container(
AdapterContainer(sessions={MySession}, handlers=[CreateUserHandler, GetUserHandler])
)
# Replace a use case's run() with a fixed return or exception
container.stub_use_case(CreateUserUseCase, returns=user)
container.stub_use_case(GetUserUseCase, raises=ValueError("not found"))
# adapt() returns a fully wired instance
uc = container.adapt(CreateUserUseCase)
result = uc.run(user_id=1, name="Alice") # → user
# Assert events and handler calls
assert result.name == "Alice"
assert container.get_handler(CreateUser).handle.call_count == 1
To control individual CommandPort/QueryPort fields inside a use case that has multiple handlers, configure handlers directly without stubbing the whole use case:
# Each handler's handle() is a MagicMock returning None by default
container.get_handler(CreateUser).handle.return_value = user
container.get_handler(GetUser).handle.side_effect = ValueError("db down")
uc = container.adapt(ManageUserUseCase)
uc.run(dto) # create.handle() returns user, get.handle() raises
get_session_stub¶
Access the stub for a given session class. Each stub method records calls and lets you configure return values:
stub = container.get_session_stub(MySession)
stub.is_dirty.return_value = True # always returns True
stub.is_dirty.side_effect = [True, False] # first True, then False
stub.is_dirty.called # True if called at least once
stub.is_dirty.call_count # number of calls
stub.is_dirty.call_args_list # list of call objects, each with .args and .kwargs
stub.begin.called # tracks begin() too
stub.commit.called # commit is called by the UseCase wrapper
get_port_stub¶
Access the stub for a given port field name. Works with any Port subclass registered on the container:
container = spy_adapter_container(AdapterContainer(logger=SpyLogger()))
stub = container.get_port_stub("logger")
stub.info.return_value = None
stub.info.called
stub.info.call_args_list
get_handler¶
Retrieve the handler for a given contract. Handler methods are also stubbed:
get_handler_stub¶
Access the handler stub for a given handler class. Works like get_port_stub for handlers:
stub = container.get_handler_stub(CreateUserHandler)
stub.handle.return_value = None
stub.handle.called
stub_use_case with returns= / raises=¶
Configure a use case stub before calling adapt. returns= stubs instance.run to return the given value; raises= makes it raise an exception:
container.stub_use_case(CreateUserUseCase, returns=42)
use_case = container.adapt(CreateUserUseCase)
result = use_case.run(user_id=1) # returns 42
stub_projection with read_returns / read_raises / write_returns / write_raises¶
Configure a projection stub before calling adapt:
container.stub_projection(MyProjection, read_returns=[], write_returns=None)
proj = container.adapt(MyProjection)
proj.read(model) # returns []
proj.write(model) # returns None
port_stub¶
For testing ports outside a container context, create stubs directly:
from aod.testing.doubles import port_stub
StubLogger = port_stub(Logger)
logger = StubLogger()
logger.info("test")
assert logger.info.called
Every stub method is a unittest.mock.MagicMock or AsyncMock. Configure return values and inspect calls with the standard mock API:
logger.info("message", user_id=1)
assert logger.info.called
assert logger.info.call_count == 1
entry = logger.info.call_args_list[0]
entry.args # ("message",)
entry.kwargs # {"user_id": 1}
Stub Control¶
Every stub method is a unittest.mock mock object:
| Property / Method | Description |
|---|---|
.return_value = value |
Always return this value |
.side_effect = exc |
Raise an exception |
.side_effect = [v1, v2] |
Return different values on successive calls |
.called |
Whether the method was called |
.call_count |
Number of calls |
.call_args_list |
List of call objects — each has .args and .kwargs |
Spy Handler Factories¶
spy_command_handler / spy_query_handler¶
from aod.testing.doubles import (
spy_command_handler,
spy_query_handler,
spy_async_command_handler,
spy_async_query_handler,
)
Factory functions that create handler port spies. Each call returns a fresh instance with its own handle() mock — no shared state.
Parameters¶
| Parameter | Type | Description |
|---|---|---|
returns |
Any |
Value that handle() returns on every call. Default: None. |
raises |
Exception \| None |
Exception that handle() raises on every call. Takes precedence over returns. |
Sync Handler Spies¶
spy = spy_command_handler(returns=user)
result = spy.handle(my_command) # returns `user`
assert spy.handle.call_count == 1
Async Handler Spies¶
spy = spy_async_command_handler(returns=user)
result = await spy.handle(my_command) # returns `user` (awaitable)
assert spy.handle.call_count == 1
Use in UseCases¶
class CreateUserUseCase(UseCase):
save_user: CommandPort[CreateUser]
def run(self, name: str) -> User:
return self.save_user.handle(CreateUser(name=name))
spy = spy_command_handler(returns=user)
uc = CreateUserUseCase(save_user=spy)
uc.run(name="Alice")
assert spy.handle.call_count == 1
Spy Classes¶
SpyLogger¶
SpyLogger is a ready-made implementation of Logger(Port):
SpyEventBus¶
spy_session¶
Create a stub class from any Session or AsyncSession subclass. Every method (including custom methods your concrete session defines) becomes a MagicMock (or AsyncMock for async sessions). is_dirty() returns False by default.
StubPg = spy_session(PgSession)
session = StubPg()
session.is_dirty.return_value = True
session.query.return_value = [{"id": 1}]
session.set.return_value = True # custom PgSession method
assert session.begin.called
assert session.commit.called
In the spy container, container.get_session_stub(Session) already does this for you.
FakeDomain¶
FakeDomain(Generic[T]) generates test data for domain objects using polyfactory.
Constructor¶
FakeDomain(model_cls: type[T], **defaults: Any)
| Parameter | Type | Description |
|---|---|---|
model_cls |
type[T] |
The domain class to build. Must be Entity, RootEntity, or ValueObject. |
**defaults |
Any |
Default values for specific fields. |
__call__¶
Build an instance with auto-generated fields for any not provided.
batch¶
Build multiple instances.
Common Testing Patterns¶
Testing Use Cases with Spy Container¶
from aod.testing.doubles import spy_adapter_container
container = spy_adapter_container(AdapterContainer(sessions={MySession}, handlers=[CreateUserHandler, GetUserHandler]))
use_case = container.adapt(CreateUserUseCase)
use_case.run(user_id=1, name="Alice")
assert_event_emitted(use_case.events, UserCreatedEvent, user_id=1)
assert container.get_handler(CreateUser).handle.called
Testing Entities¶
from aod.testing import build, events_of, assert_event_emitted
class User(RootEntity):
id: int
name: str
def __post_init__(self) -> None:
self._event_emitter.emit(UserCreatedEvent(user_id=self.id))
user = build(User, id=1, name="Alice")
events = events_of(user)
assert_event_emitted(events, UserCreatedEvent, user_id=1)
Testing Value Objects¶
from aod.testing import build
class Address(ValueObject):
street: str
city: str
addr = build(Address, street="123 Main", city="NYC")