RSpec matchers
Adding matchers to project
Add this line to your application's Gemfile
:
group :test do
gem "ruby_event_store-rspec"
end
Event matchers
be_event
The be_event
matcher enables you to make expectations on a domain event. It exposes fluent interface.
OrderPlaced = Class.new(RubyEventStore::Event)
domain_event =
OrderPlaced.new(data: { order_id: 42, net_value: BigDecimal.new("1999.0") }, metadata: { remote_ip: "1.2.3.4" })
expect(domain_event).to(
be_an_event(OrderPlaced)
.with_data(order_id: 42, net_value: BigDecimal.new("1999.0"))
.with_metadata(remote_ip: "1.2.3.4"),
)
By default the behaviour of with_data
and with_metadata
is not strict, that is the expectation is met when all specified values for keys match. Additional data or metadata that is not specified to be expected does not change the outcome.
domain_event = OrderPlaced.new(data: { order_id: 42, net_value: BigDecimal.new("1999.0") })
# this would pass even though data contains also net_value
expect(domain_event).to be_an_event(OrderPlaced).with_data(order_id: 42)
This matcher is both composable and accepting built-in matchers as a part of an expectation.
expect(domain_event).to be_an_event(OrderPlaced).with_data(order_id: kind_of(Integer))
expect([domain_event]).to include(an_event(OrderPlaced))
If you depend on matching the exact data or metadata, there's a strict
modifier.
domain_event = OrderPlaced.new(data: { order_id: 42, net_value: BigDecimal.new("1999.0") })
# this would fail as data contains unexpected net_value
expect(domain_event).to be_an_event(OrderPlaced).with_data(order_id: 42).strict
Mind that strict
makes both with_data
and with_metadata
behave in a stricter way. If you need to mix both, i.e. strict data but non-strict metadata then consider composing matchers.
expect(domain_event)
.to(be_event(OrderPlaced).with_data(order_id: 42, net_value: BigDecimal.new("1999.0")).strict
.and(an_event(OrderPlaced).with_metadata(timestamp: kind_of(Time)))
You may have noticed the same matcher being referenced as be_event
, be_an_event
and an_event
. There's also just event
. Use whichever reads better grammatically.
Event store matchers
have_published
Use this matcher to target event_store
and reading from streams specifically.
In a simplest form it would read all streams forward and check whether the expectation holds true. Its behaviour can be best compared to the include
matcher — it is satisfied by at least one element present in the collection. You're encouraged to compose it with be_event
.
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
event_store.publish(OrderPlaced.new(data: { order_id: 42 }))
expect(event_store).to have_published(an_event(OrderPlaced))
Expectation can be narrowed to the specific stream(s).
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
event_store.publish(OrderPlaced.new(data: { order_id: 42 }), stream_name: "Order$42")
expect(event_store).to have_published(an_event(OrderPlaced)).in_stream("Order$42")
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
event_store.publish(event = OrderPlaced.new(data: { order_id: 42 }), stream_name: "Order$42")
event_store.link(event.event_id, stream_name: "SalesReport2021")
expect(event_store).to have_published(an_event(OrderPlaced)).in_streams(%w[Order$42 SalesReport2021])
It is sometimes important to ensure that specific amount of events of given type have been published. Luckily there's a modifier to cover that usecase.
expect(event_store).not_to have_published(an_event(OrderPlaced)).once
expect(event_store).to have_published(an_event(OrderPlaced)).exactly(2).times
You can make expectation on several events at once.
expect(event_store).to have_published(
an_event(OrderPlaced),
an_event(OrderExpired).with_data(expired_at: be_between(Date.yesterday, Date.tomorrow)),
)
You can also make expectation to ensure that expected list of events is exact actual list of events (and in the same order) using strict
modifier.
expect(event_store).to have_published(
an_event(OrderPlaced),
an_event(OrderExpired).with_data(expired_at: be_between(Date.yesterday, Date.tomorrow)),
).strict
Last but not least, you can specify reading starting point for matcher.
expect(event_store).to have_published(an_event(OrderExpired)).from(order_placed.event_id)
If there's a usecase not covered by examples above or you need a different set of events to make expectations on you can always resort to a more verbose approach and skip have_published
.
expect(event_store.read.stream("OrderAuditLog$42").limit(2)).to eq([an_event(OrderPlaced), an_event(OrderExpired)])
publish
This matcher is similar to have_published
one, but targets only events published in given execution block.
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
expect { event_store.publish(OrderPlaced.new(data: { order_id: 42 })) }.to publish(an_event(OrderPlaced)).in(
event_store,
)
Expectation can be narrowed to the specific stream(s).
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
expect { event_store.publish(OrderPlaced.new(data: { order_id: 42 }), stream_name: "Order$42") }.to publish(
an_event(OrderPlaced),
)
.in(event_store)
.in_stream("Order$42")
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
expect {
event_store.publish(event = OrderPlaced.new(data: { order_id: 42 }), stream_name: "Order$42")
event_store.link(event.event_id, stream_name: "SalesReport2021")
}.to publish(an_event(OrderPlaced)).in(event_store).in_streams(["Order$42", "SalesReport2021")
It is sometimes important to ensure that specific amount of events of given type have been published. Luckily there's a modifier to cover that usecase.
expect { event_store.publish(OrderPlaced.new(data: { order_id: 42 }), stream_name: "Order$42") }.to publish(
an_event(OrderPlaced),
)
.once
.in(event_store)
expect {
event_store.publish(OrderPlaced.new(data: { order_id: 42 }), stream_name: "Order$42")
event_store.publish(OrderPlaced.new(data: { order_id: 42 }), stream_name: "Order$42")
}.to publish(an_event(OrderPlaced)).exactly(2).times.in(event_store)
You can make expectation on several events at once.
expect {
# ...tested code here
}.to publish(
an_event(OrderPlaced),
an_event(OrderExpired).with_data(expired_at: be_between(Date.yesterday, Date.tomorrow)),
).in(event_store)
have_subscribed_to_events
Use this matcher to make sure that a handler has or has not subscribed to event types in target event_store
.
Ensuring handler is subscribed to given event types:
event_store = RubyEventStore::Client.new(repository: RubyEventStore::InMemoryRepository.new)
expect(Handler).to have_subscribed_to_events(FooEvent, BarEvent).in(event_store)
Checking if handler does not subscribe to any of given event types:
expect(Handler).not_to have_subscribed_to_events(FooEvent, BarEvent).in(event_store)
Aggregate root matchers
The matchers described below are intended to be used on aggregate root gem.
To explain the usage of matchers sample aggregate class is defined:
OrderSubmitted = Class.new(RubyEventStore::Event)
OrderExpired = Class.new(RubyEventStore::Event)
class Order
include AggregateRoot
HasBeenAlreadySubmitted = Class.new(StandardError)
HasExpired = Class.new(StandardError)
def initialize
self.state = :new
# any other code here
end
def submit
raise HasBeenAlreadySubmitted if state == :submitted
raise HasExpired if state == :expired
apply OrderSubmitted.new(data: { delivery_date: Time.now + 24.hours })
end
def expire
apply OrderExpired.new
end
private
attr_accessor :state
on OrderSubmitted do |event|
self.state = :submitted
end
on OrderExpired do |event|
self.state = :expired
end
end
The matchers behaviour is almost identical to have_published
and publish
counterparts, except the concept of stream. Expecations are made against internal unpublished events collection.
have_applied
This matcher check if an expected event has been applied in aggregate_root
object.
aggregate_root = Order.new
aggregate_root.submit
expect(aggregate_root).to have_applied(event(OrderSubmitted))
You could define expectations how many events have been applied by using:
expect(aggregate_root).to have_applied(event(OrderSubmitted)).once
expect(aggregate_root).to have_applied(event(OrderSubmitted)).exactly(3).times
With strict
option it checks if only expected events have been applied.
expect(aggregate_root).to have_applied(event(OrderSubmitted)).strict
apply
This matcher is similar to have_applied
. It check if expected event is applied
in given aggregate_root
object but only during execution of code block.
aggregate_root = Order.new
aggregate_root.submit
expect { aggregate_root.expire }.to apply(event(OrderExpired)).in(aggregate_root)
You could define expectations how many events have been applied by using:
expect { aggregate_root.expire }.to apply(an_event(OrderExpired)).once.in(aggregate_root)
expect do
aggregate_root.expire
aggregate_root.expire
aggregate_root.expire
end.to apply(an_event(OrderExpired)).exactly(3).times.in(aggregate_root)
With strict
option it checks if only expected events have been applied in given execution block.
aggregate_root = Order.new
aggregate_root.submit
expect { aggregate_root.expire }.to apply(event(OrderExpired)).in(aggregate_root).strict