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[BaseAggregate]",
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
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | |
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: Any
The raw aggregate instance, if needed directly.
after
after(*events: Any) -> 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
295 296 297 298 299 300 301 302 303 304 305 | |
process
process(
command: Any, *, 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
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 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 | |
__getattr__
__getattr__(name: str) -> Any
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
430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 | |
_seed_events
_seed_events(domain: '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
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 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 | |
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
201 202 | |
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
204 205 206 | |
__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
208 209 210 211 212 213 214 215 216 217 218 | |
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
220 221 222 223 224 225 | |
of_type
of_type(event_cls: type) -> list[Any]
Return all events of the given type.
Source code in src/protean/testing.py
227 228 229 | |
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
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | |