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.