Testing DSL
Fluent test helpers for event-sourced aggregates. The given function
provides a Pythonic DSL for integration tests that exercise the full command
processing pipeline: command -> handler -> aggregate -> events.
See Testing guide for practical usage.
Event-sourcing test DSL for Protean.
Provides a fluent, Pythonic DSL for testing event-sourced aggregates through integration tests that exercise the full command processing pipeline: command → handler → aggregate → events.
The three words::
given(Order, order_created, order_confirmed).process(initiate_payment)
"Given an Order after order_created and order_confirmed, process initiate_payment."
After .process(), assert with plain Python::
assert order.accepted
assert PaymentPending in order.events
assert order.events[PaymentPending].payment_id == "pay-001"
assert order.status == "Payment_Pending"
Multi-command chaining::
order = (
given(Order)
.process(CreateOrder(order_id=oid, customer="Alice", amount=99.99))
.process(ConfirmOrder(order_id=oid))
.process(InitiatePayment(order_id=oid, payment_id="pay-001"))
)
assert order.accepted
assert order.status == "Payment_Pending"
Usage::
from protean.testing import given
def test_payment_on_confirmed_order(order_created, order_confirmed, initiate_payment):
order = given(Order, order_created, order_confirmed).process(initiate_payment)
assert order.accepted
assert PaymentPending in order.events
assert order.events[PaymentPending].payment_id == "pay-001"
assert order.status == "Payment_Pending"
def test_cannot_pay_unconfirmed_order(order_created, initiate_payment):
order = given(Order, order_created).process(initiate_payment)
assert order.rejected
assert isinstance(order.rejection, ValidationError)
assert len(order.events) == 0
def test_create_order(create_order):
order = given(Order).process(create_order)
assert order.accepted
assert OrderCreated in order.events
assert order.status == "Created"
AggregateResult
AggregateResult(aggregate_cls: type, given_events: list[Any] | None = None)
The result of processing a command against an event-sourced aggregate.
Proxies attribute access to the underlying aggregate, so
order.status works directly.
Supports multi-command chaining — call .process() repeatedly
to build up aggregate state through the real pipeline::
order = (
given(Order)
.process(CreateOrder(order_id=oid, customer="Alice", amount=99.99))
.process(ConfirmOrder(order_id=oid))
.process(InitiatePayment(order_id=oid, payment_id="pay-001"))
)
Created by given(), not directly.
Source code in src/protean/testing.py
192 193 194 195 196 197 198 199 200 201 202 203 204 | |
rejection
property
rejection: Exception | None
The exception if the command was rejected, or None.
accepted
property
accepted: bool
True if the last command was processed without exception.
rejected
property
rejected: bool
True if the last command raised an exception.
rejection_messages
property
rejection_messages: list[str]
Flat list of error messages from the rejection.
For ValidationError, flattens the messages dict values.
For other exceptions, returns [str(exc)].
Returns [] if no rejection.
Examples::
assert "Order must be confirmed" in result.rejection_messages
aggregate
property
aggregate
The raw aggregate instance, if needed directly.
after
after(*events) -> AggregateResult
Accumulate more history events (for BDD "And given" steps).
Returns self for chaining::
order = given(Order, order_created)
order = order.after(order_confirmed)
order = order.after(payment_pending)
Source code in src/protean/testing.py
206 207 208 209 210 211 212 213 214 215 216 | |
process
process(command, *, correlation_id: str | None = None) -> AggregateResult
Dispatch a command through the domain's full processing pipeline.
Seeds the event store with given events (on first call only),
then calls domain.process(command) which routes through the
real command handler, repository, and unit of work.
Can be called multiple times to chain commands::
result = (
given(Order)
.process(CreateOrder(...))
.process(ConfirmOrder(...))
)
After each call:
.eventscontains events from the last command only..all_eventscontains events from all commands..accepted/.rejectedreflects the last command.
Returns self for chaining.
Source code in src/protean/testing.py
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | |
__getattr__
__getattr__(name: str)
Proxy attribute access to the underlying aggregate.
This makes order.status, order.items, order.pricing
work directly on the result object.
Source code in src/protean/testing.py
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 | |
_seed_events
_seed_events(domain) -> Any
Write given events to the event store and process handlers.
Reconstitutes the aggregate from events to determine its identity,
then enriches each event with proper metadata and appends to the
event store so that domain.process() can load the aggregate
via its repository.
Also runs synchronous event handlers (projectors, etc.) for each seeded event, mirroring what UoW commit does. This ensures projections and other side effects are in place when the command under test is processed.
Returns the aggregate identifier.
Source code in src/protean/testing.py
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 | |
EventLog
EventLog(events: list[Any])
A collection of domain events with Pythonic access.
Supports in (contains by type), [] (getitem by type or index),
len, bool, iteration, .get(), .of_type(), .types,
.first, and .last.
Examples::
assert PaymentPending in log
assert log[PaymentPending].payment_id == "pay-001"
assert log.get(PaymentFailed) is None
assert log.types == [PaymentPending]
assert len(log) == 1
assert log.first is placed_event
assert log # truthy when non-empty
Source code in src/protean/testing.py
114 115 | |
types
property
types: list[type]
Ordered list of event types.
first
property
first: Any | None
First event, or None if empty.
last
property
last: Any | None
Last event, or None if empty.
__contains__
__contains__(event_cls: type) -> bool
Check if an event of this type exists.
Source code in src/protean/testing.py
117 118 119 | |
__getitem__
__getitem__(key: type | int) -> Any
Access by event class (first match) or by index.
Raises KeyError if an event class is not found.
Source code in src/protean/testing.py
121 122 123 124 125 126 127 128 129 130 131 | |
get
get(event_cls: type, default: Any = None) -> Any
Safe access by event class. Returns default if not found.
Source code in src/protean/testing.py
133 134 135 136 137 138 | |
of_type
of_type(event_cls: type) -> list[Any]
Return all events of the given type.
Source code in src/protean/testing.py
140 141 142 | |
given
given(aggregate_cls: type, *events: 'BaseEvent') -> 'AggregateResult'
Start an event-sourcing test sentence.
| PARAMETER | DESCRIPTION |
|---|---|
aggregate_cls
|
The aggregate class under test.
TYPE:
|
*events
|
Past domain events constituting the aggregate's history.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
AggregateResult
|
Result object ready for
TYPE:
|
Examples::
given(Order) # no history
given(Order, order_created) # one event
given(Order, order_created, order_confirmed) # multiple events
Source code in src/protean/testing.py
77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 | |