aggregateSpec
Specifies an aggregate's behaviour as given events, a command and the expected outcome, against the real kernel and an in-memory log:
aggregateSpec(cartModel) {
given(CartCreated("ahoo"))
whenCommand(AddItem("apple", 1)) {
expectEvents(ItemAdded("apple", 1))
expectState { items.assert().containsEntry("apple", 1) }
}
}The kernel checks that deciding never changes the state: a decide function or after hook that assigns state fails the specification.
Time stands still at start until AggregateSpec.advanceTime moves it: the time a decision reads (me.ahoo.wow.api.decide.DecideScope.now), which its commit is recorded at, and the due times of the commands the aggregate schedules, are read from it. Advancing the time sends the aggregate the commands it scheduled for itself that are then due, as the runtime would, each with its timer's request id (timer#slot) and its slot (me.ahoo.wow.api.decide.Command.schedule). A cron rule steps through its slots as the runtime's firing loop does: advancing past several of them sends only the latest, the others counted as skipped, unless the rule catches up (me.ahoo.wow.api.schedule.Misfire.CATCH_UP), which sends each in turn:
aggregateSpec(orderModel) {
whenCommand(PlaceOrder(...)) {
expectScheduled("pay-timeout", CancelOrder("unpaid"), ScheduleRule.after(30.minutes))
}
advanceTime(30.minutes) { expectEvents(OrderCancelled("unpaid")) }
}The ids a decide function takes (me.ahoo.wow.api.decide.DecideScope.ids) are SequentialIds: id-1, id-2… and <aggregate>-1… for aggregate ids, counted across the specification.
Scenarios that share a prefix AggregateSpec.fork from it, at the current point, at one marked with AggregateSpec.ref, or after a command from inside its checks (Expectations.fork): each fork runs on its own copy of the aggregate, its log, timers, reservations, ids and time, so its commands do not reach the others, and nothing before the fork runs again: a domain service the decide functions call is not called again for the prefix.
The commands are sent by caller, as the command gateway admits a caller's command: in its scope, by its operator, with its application as the request fact app_id (CallerAdmission); none (an anonymous caller in no scope) by default. The aggregate is in tenantId, the caller's tenant by default.
The whole specification is one test; aggregateSpecTests makes the same specification a tree of tests instead.