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