Open clarissalimab opened 3 years ago
@clarissalimab this is a really good start! I have a few thoughts/questions you may want to answer:
have_published_events of_types: [:payment, :transaction], :regardless_of_order, :and_any_others
doesn't work in Ruby. The problem is that non-hash arguments need to be before hash arguments.have_published_events
? have_published_events
? Or even just a single non-hash argument?have_published_events of_types: [:payment, :transaction], :regardless_of_order, :and_any_others doesn't work in Ruby. The problem is that non-hash arguments need to be before hash arguments.
Hmm, I didn't realize that. Changing the order won't look that great. How about the options would come in a hash? For example:
it 'publishes events in any order and any others' do
is_expected
.to have_published_events of_types: [:payment, :transaction],
options: [:regardless_of_order, :and_any_others]
end
We could also get rid of the key arguments. For example:
it 'publishes events in any order and any others' do
is_expected
.to have_published_events [:payment, :transaction], :regardless_of_order, :and_any_others
end
What happens if the user passes nothing to have_published_events?
My thinking is that it shouldn't be allowed. At first I thought that maybe it could just test if any event was fired, but it seems very broad. What do you think?
What happens if the user passes non-hash arguments to have_published_events? Or even just a single non-hash argument?
It could be smart and assume that if it's a symbol, then it's an event type, and if it's a hash that does not have the with_data
key, it could assume that it is an entire object event.
My thinking is that it shouldn't be allowed. At first I thought that maybe it could just test if any event was fired, but it seems very broad. What do you think?
Let's not allow it for now.
It could be smart and assume that if it's a symbol, then it's an event type, and if it's a hash that does not have the
with_data
key, it could assume that it is an entire object event.
I'd change the list of events names from a symbol to a string. I think it makes more sense to have it be "payment.created" than :payment_created
. I think we should eventually get rid of those symbols from the whole event publishing system but that's a separate issue.
Is there any reason they'd need the with_data
key?
Something still feels off about the design though. I'm not quite sure what it is. Is there any way we can use methods on the return of have_published_event
to make this feel more natural?
Is there any reason they'd need the with_data key?
It's not needed, I just made it to look like a phrase.
Something still feels off about the design though. I'm not quite sure what it is. Is there any way we can use methods on the return of have_published_event to make this feel more natural?
That's a good idea! I'll try to rewrite it to see.
I like where this is a going. There are a few things:
I'm wondering if there's a way we can use blocks to describe more complex scenarios. Like consider the following:
it { is_expected.to have_published_events('transaction.created', 'payment.created') }
which means verify that a transaction.created
event was followed by a payment.created
with any other events before or after
Here transaction.created
and payment.created
could be also be expected objects to match against
it { is_expected.to have_published_events_starting_with('transaction.created', 'payment.created') }
which means verify that the first event transaction.created
event was followed by a payment.created
with no events before
it { is_expected.to have_published_events_ending_with('transaction.created', 'payment.created') }
which means verify that the event transaction.created
event was followed by a payment.created
with no events after
it { is_expected.to have_published_events_ending_with('transaction.created', 'payment.created') }
which means verify that the first event transaction.created
event was followed by the last event payment.created
it {is_expected.to have_published_events('transaction.created') do |event_manager, subject| # you may want the subject in the block
expect(event_manager.next).to be_event('payment.created') # be_event matches using same logic as have_published_events
expect(event_manager.next).to be_event('stripe_charge.created')
# could have other expectations in here
end}
which means verify that an transaction.created
occurred followed directly by a payment.created
, followed directly by stripe_charge.created
with any other events before or after
it {is_expected.to have_published_events('transaction.created') do |event_manager, subject|
expect(event_manager.next).to be_event('payment.created')
end.and have_published_event('stripe_charge.created') # instead of have_published_event you could use have_published_event_starting_with or have_published_event_ending_with
}
which means verify that an transaction.created
occurred followed directly by a payment.created
and then, at some point, followed by a stripe_charge.created
.
it {is_expected.to have_published_events('transaction.created') do |event_manager, subject|
expect(event_manager.next).to be_event('payment.created')
expect(event_manager.next).to be_event('stripe_charge.created')
end.and have_published_events('supporter.created') do |event_manager, subject|
expect(event_manager.next).to be_event('supporter_address.created')
end.and have_published_event('ticket.created')
}
This means:
transaction.created
occurredpayment.created
stripe_charge.created
supporter.created
supporter_address.created
In effect: have_published_event
(or have_published_events
) means just keep going from the last place we were until you find something you were looking for, with the caveats that have_published_events_starting_with
and have_published_events_ending_with
means be more specific.
ORRRR maybe there's a way to create methods that allow us to combine them like:
it {is_expected.to have_published_event('transaction.created')
.followed_by('payment.created') # match on the next immediate event and followed_by uses the same matching as have_published_event
.followed_by('stripe_charge.created')
.and have_published_event('supporter.created')
.followed_by('supporter_address.created')
.and have_published_event('ticket.created')
}
This means the same as the previous example.
when you use an event type without the verb, i.e. "transaction" instead of "transaction.created", what should that do?
I think it should fail. We could create a method that only verifies the event type without considering the verb, but I don't know the use case for that.
let's say I want to know that three particular events happened in order with no events in between and then, any number of other events, and then two particular events, and then any other events. How do I write that?
If we followed my original idea, I think this should be in different expectations. It would look like:
describe 'an action that fires multiple events' do
subject do
described_class
.create(
nonprofit: nonprofit,
supporter: supporter,
subtransactions: [subtransaction]
)
end
it 'publishes events in order and any others' do
is_expected
.to have_published_events.of_types(['payment.created', 'transaction.updated'])
.and_any_others
end
it 'publishes some particular events in any order along with others' do
is_expected
.to have_published_events.of_types(['payment.created', 'transaction.refunded'])
.in_any_order
.and_any_others
end
end
In that case, though, it wouldn't be aware of the chronology of the events, like:
some don't care events
> specific events in order
> some more don't care events that include specific events
.
I like the solutions you thought of for that. The second approach looks more friendly to me.
@clarissalimab if we like the second one, please take a crack at fleshing that out and documenting it.
@wwahammy one thing that came to my mind when I was updating the description: by default, we would allow that other events are fired without being described in the tests? What if some unexpected event is being fired?
Let's use these methods from my previous idea for this:
have_published_events_ending_with
-- make sure these are the last eventshave_published_events_starting_with
-- make sure these are the first eventsFor situations where we want no other events fired other than what's listed... what kind of method name should we use?
@wwahammy hmm, what about .have_only_published_events
or .only
at the end? Like:
it 'fires transaction.created, payment.created, subtransaction.created and offline_transaction.created in that order' do
is_expected
.to have_only_published_events('transaction.created')
.followed_by('payment.created')
.followed_by('subtransaction.created')
.followed_by('offline_transaction.created')
end
or:
it 'fires transaction.created, payment.created, subtransaction.created and offline_transaction.created in that order' do
is_expected
.to have_published_events('transaction.created')
.followed_by('payment.created')
.followed_by('subtransaction.created')
.followed_by('offline_transaction.created')
.only
end
I think have_only_published_events
seems like the best.
@wwahammy I updated the description with your suggestions! :smile_cat:
Make sure to document the use of a compound expectation, i.e have_published_events('transaction.created').and have_published_events('payment.created')
which means 'transaction.created' was published and at some point in the future, 'payment.created' was published.
@wwahammy I forgot that one, updated again :P
@clarissalimab do you think my edits makes sense?
@wwahammy yes, I think so
So after, playing a bit with how to implement this, I think we need to change .and
to something else. The problem is that .and
means something slightly different in RSpec matchers already. I'm thinking we could change it to something like .and_later
but maybe there's a better name. It's also possible we could use followed_by
for the situation where we need the event to eventually happen but not immediately and then use a different method for what the functionality we've currently documented for followed_by
.
What do you think @clarissalimab?
@wwahammy I like .and_later
better, .followed_by
for me suggests that it happens immediately after
Sweet, let's switch it to that in the issue
Given a subject, we need a matcher that evaluates if an event was fired under the expected type, object, order, etc.
Reference: ActiveJob matcher.
Criterias
The matcher should have the abilities to:
Object event description
An object event description is:
Methods
.have_published_event
/.have_published_events
receives one object event description and checks if the event like that was published;.have_published_events_starting_with
receives object event description and checks if the given event was the first one published;.have_published_events_ending_with
receives object event description and checks if the given event was the last one published;.have_only_published_event
/.have_only_published_events
checks if the only events published are the ones described in the expectation.followed_by
receives object event description and checks if the given event happened directly followed by the prior event that was provided - must be preceded by a.have_published_events
or.have_only_published_events
;.and_later
allows you to combine multiple sets of expectations in order: a compound expectationExamples
One event
Multiple events
In some situations an action fires multiple events. The scenarios that we want to be able to test are: