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.
Testing DSL for Protean.
Provides fluent, Pythonic DSLs for testing event-sourced aggregates, process managers, projections, and domain invariants.
Event-sourcing tests
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"
Process manager tests
When the first argument is a process manager class, given() returns
a ProcessManagerResult that feeds events through the PM's handlers::
result = given(
OrderFulfillmentPM,
OrderPlaced(order_id="o1", customer_id="c1", total=100.0),
PaymentConfirmed(payment_id="p1", order_id="o1", amount=100.0),
)
assert result.status == "awaiting_shipment"
assert not result.is_complete
assert result.transition_count == 2
Or events first with .results_in()::
result = given(
OrderPlaced(order_id="o1", ...),
PaymentConfirmed(order_id="o1", ...),
).results_in(OrderFulfillmentPM, id="o1")
Projection tests
When called with event instances only (no class), given() returns
an EventSequence for testing projections::
result = given(
Registered(user_id="u1", name="Alice"),
Transacted(user_id="u1", amount=100),
).then(Balances, id="u1")
result.has(name="Alice", balance=100)
assert result.projection.balance == 100
To test invariants, use pytest.raises(ValidationError) directly.
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
235 236 237 238 239 240 241 242 243 244 245 246 247 | |
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
249 250 251 252 253 254 255 256 257 258 259 | |
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
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 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 | |
__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
389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 | |
_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
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 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 | |
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
157 158 | |
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
160 161 162 | |
__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
164 165 166 167 168 169 170 171 172 173 174 | |
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
176 177 178 179 180 181 | |
of_type
of_type(event_cls: type) -> list[Any]
Return all events of the given type.
Source code in src/protean/testing.py
183 184 185 | |
given
given(
cls_or_event: type | "BaseEvent", *events: "BaseEvent"
) -> AggregateResult | ProcessManagerResult | EventSequence
Start a test sentence.
Polymorphic entry point:
given(AggregateClass, *events)— returns anAggregateResultfor event-sourcing tests.given(ProcessManagerClass, *events)— returns aProcessManagerResultfor process manager tests.given(event, *events)— returns anEventSequencefor projection or process manager tests (via.results_in()).
Examples::
# Event-sourcing test
given(Order) # no history
given(Order, order_created) # one event
given(Order, order_created, order_confirmed) # multiple events
# Process manager test
given(OrderFulfillmentPM, order_placed, payment_confirmed)
# Projection test
given(Registered(user_id="u1", name="Alice"))
given(registered_event, transacted_event)
Source code in src/protean/testing.py
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | |