Testing database transactions explicitly with RSpec

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

Published by

Paweł Gościcki

Ruby/Rails programmer.