RSpec matchers
Adding matchers to the project
Add this line to your application's Gemfile:
group :test do
gem "rails_event_store-rspec"
end
Matchers usage
be_event
The be_event
matcher enables you to make expectations on a domain event. It exposes fluent interface.
OrderPlaced = Class.new(RailsEventStore::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.
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 = RailsEventStore::Client.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.
event_store = RailsEventStore::Client.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")
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 = RailsEventStore::Client.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.
event_store = RailsEventStore::Client.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")
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)
AggregateRoot matchers
The matchers described below are intended to be used on aggregate root.
To explain the usage of matchers sample aggregate class is defined:
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
def apply_order_submitted(event)
self.state = :submitted
end
def apply_order_expired(event)
self.state = :expired
end
end
The matchers behaviour is almost identical to have_published
& 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)
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