Testing an Event Sourced application
… and check why 5600+ Rails engineers read also this
Testing an Event Sourced application
Some time ago I’ve published a sample application showing how to build a simple event sourced application using Rails & RES. But there was a big part missing there - the tests.
My sample uses CQRS approach to handle all operations.
That means the control flow is as follow:
- A command is created based on params from UI
- Command is handled by a command handler:
- based on command’s aggregate id all events for an aggregate are loaded from RES and aggregate state is recreated
- a domain object method is called that will produce new domain events
- domain event are applied to the aggregate
- domain events are stored in RES & published to event handlers
AAA
This is a basic pattern how good test should be created. There are 3 parts: Arrange - when you setup initial state for a test, Act - where you perform actual operation you want to test and Assert - when you check results.
And the AAA pattern should be preserved for Event Sources application.
Given a series of events
How to build an initial state when you don’t have a state?
This should be quite easy. Any state is a derivative of domain events. You could build any state by applying domain events.
To build a state you just need some events:
include CommandHandlers::TestCase
test 'order is created' do
event_store = FakeEventStore.new
aggregate_id = SecureRandom.uuid
customer_id = 1
order_number = "123/08/2015"
arrange(event_store,
[Events::ItemAddedToBasket.create(aggregate_id, customer_id)])
# ...
end
# ./test/lib/command_handlers/test_case.rb
module CommandHandlers
class FakeEventStore
def initialize
@events = []
@published = []
end
attr_reader :events, :published
def publish_event(event, aggregate_id)
events << event
published << event
end
def read_all_events(aggregate_id)
events
end
end
class FakeNumberGenerator
def call
"123/08/2015"
end
end
module TestCase
# ...
def arrange(event_store, events)
event_store.events.concat(events)
end
# ...
end
end
Then we have our test state arranged. Notice that I’ve used fake event store & domain services to avoid dependencies and have really fast tests.
When a command
In Event Sourced application act (operation we want to test) is usually handling of a command. To do it you just need a command, you need the command handler and then just dispatch the command to the command handler.
test 'order is created' do
# ...
act(event_store,
Command::CreateOrder.new(order_id: aggregate_id, customer_id: customer_id))
# ...
end
# ./test/lib/command_handlers/test_case.rb
module CommandHandlers
# ...
module TestCase
include Command::Execute
# ...
def act(event_store, command)
execute(command, **dependencies(event_store))
end
# ...
private
def dependencies(event_store)
{
repository:
RailsEventStore::Repositories::AggregateRepository.new(event_store),
number_generator:
FakeNumberGenerator.new
}
end
end
end
The same Command::Execute
module is used in ApplicationController
to dispatch real commands to the system.
Expect a series or events
You should not assert on the current state, actually you should not rely on a state at all. All you need to verify is if the correct domain events have been produced.
test 'order is created' do
# ...
assert_changes(event_store,
[Events::OrderCreated.create(aggregate_id, order_number, customer_id)])
end
# ./test/lib/command_handlers/test_case.rb
module CommandHandlers
# ...
module TestCase
# ...
def assert_changes(event_store, expected)
actuals = event_store.published.map(&:data)
expects = expected.map(&:data)
assert_equal(actuals, expects)
end
def assert_no_changes(event_store)
assert_empty(event_store.published)
end
end
end
And because all state is a result of events checking what have been produced has a nice side effect. You test if all expected domain events have been produced and if only the ones expected. In that case, you test if any unexpected change have not been introduced.
or an exception
Remember that any command may end up with an error. There could be various reasons, technical ones (oh no! regression again?), or error could be just a result of some business rules validations.
Complete code sample for blog post could be found here.