TL;DR; you cannot do it reliably with RSpec.
The long story goes like this. Lets say you have a code executing an AR rollback when something fails:
def call
Model.transaction do
update_reason
unless send_notification
raise ActiveRecord::Rollback
end
end
end
This update_reason
is a block of code, which does some database operation, like an INSERT
or UPDATE
:
def update_reason
object.update reason: reason
end
And send_notification
is just some external API call.
So when you write a spec for this code, you might want to write something like this:
describe '#call' do
it 'does not update the reason when sending the notification fails' do
allow(object).to receive(:send_notification).and_return false
expect {
object.call
}.not_to change(object, :reason)
end
And, surprise, surprise, the above spec will fail! The reason
will change on the object, even though the logic says it should not.
Why is that? This is because normally you have your whole example spec wrapped in a transaction and rolled back after the example has been run. Since your code opens up a new, nested transaction internally (with the #call
method: Model.transaction do
). This messes things up and now the rollback in the nested transaction does not really roll back anything. Adding require_new: true
doesn’t help. Disabling transaction just for this one spec does not work either. Unfortunately.
Something like this works, but it’s not ideal:
expect {
object.call
}.to raise_exception ActiveRecord::Rollback
Additional reading:
* How to test that a certain function uses a transaction in Rails