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.