34

I have a model Lead and a callback: after_commit :create, :send_to_SPL

I am using Rails-4.1.0, ruby-2.1.1, RSpec.

1) This spec is not passing:

context 'callbacks' do
  it 'shall call \'send_to_SPL\' after create' do
    expect(lead).to receive(:send_to_SPL)
    lead = Lead.create(init_hash)
    p lead.new_record? # => false
  end
end

2) This spec is not passing too:

context 'callbacks' do
  it 'shall call \'send_to_SPL\' after create' do
    expect(ActiveSupport::Callbacks::Callback).to receive(:build)
    lead = Lead.create(init_hash)
  end
end

3) This one is passing, but I think it is not testing after_commit callback:

context 'callbacks' do
  it 'shall call \'send_to_SPL\' after create' do
    expect(lead).to receive(:send_to_SPL)
    lead.send(:send_to_SPL)
  end
end

What is the best way to test after_commit callbacks in Rails?

Mihail Davydenkov
  • 1,861
  • 2
  • 19
  • 33

5 Answers5

60

I thought Mihail Davydenkov's comment deserved to be an answer:

You can also use subject.run_callbacks(:commit).

Also note that this issue (commit callbacks not getting called in transactional tests) should be fixed in rails 5.0+ so you may wish to make a note to remove any workarounds you may use in the meantime when you upgrade. See: https://github.com/rails/rails/pull/18458

Community
  • 1
  • 1
ihaztehcodez
  • 2,123
  • 15
  • 29
  • 1
    I doesn't work on `4.2.7` if callback defined with `on: :update`. I had to also do `subject.instance_variable_get(:@_start_transaction_state)[:new_record]=false` before running callbacks to make it work – Dmitriy Budnik Oct 20 '16 at 10:39
  • Though it executes the callback but self.previous_changes are { } which is not true. How to get that? – Imran Ahmad Jul 27 '18 at 11:53
40

Try to use test_after_commit gem

or add following code in spec/support/helpers/test_after_commit.rb - Gist

Oleg Haidul
  • 3,682
  • 1
  • 22
  • 14
  • 38
    You also can use `subject.run_callbacks(:commit)` in the end of the body of the block `it` – Mihail Davydenkov Dec 27 '14 at 11:24
  • 2
    This gem works fantastic for Rails 3 and Rails 4. In Rails 5, it's no longer necessary because it's been added to the core: https://github.com/rails/rails/pull/18458/commits/eb72e349b205c47a64faa5d6fe9f831aa7fdddf3 – Joshua Pinter Dec 03 '18 at 06:18
12

I'm using DatabaseCleaner, with a configuration where I can easily switch between transaction and truncation, where the former is preferred, because of speed, but where the latter can be used for testing callbacks.

RSpec before and after handlers work with scopes, so if you want to make truncation a scope, define a before handler;

config.before(:each, truncate: true) do
  DatabaseCleaner.strategy = :truncation
end

And now to use this configuration for a describe, context or it block, you should declare it like:

describe "callbacks", truncate: true do
   # all specs within this block will be using the truncation strategy
  describe "#save" do
    it "should trigger my callback" do
      expect(lead).to receive(:send_to_SPL)
      lead = Lead.create(init_hash)
    end
  end
end

Complete hook configuration: (store in spec/support/database_cleaner.rb)

RSpec.configure do |config|
  config.before(:suite) do
    DatabaseCleaner.clean_with(:truncation)
  end

  config.before(:each) do
    DatabaseCleaner.strategy = :transaction
  end

  config.before(:each, truncate: true) do
    DatabaseCleaner.strategy = :truncation
  end

  config.before(:each) do
    DatabaseCleaner.start
  end

  config.append_after(:each) do
    DatabaseCleaner.clean
  end
end
Koen.
  • 25,449
  • 7
  • 83
  • 78
4

Update for Rails5.

Callback handling has indeed been fixed, but you may still need to use #reload liberally.

An example:
Given a model that defines an after-create callback like so:

after_create_commit { assign_some_association }

You can spec this behavior with:

describe "callbacks" do
  describe "assigning_some_association" do
    subject(:saving) { record.save!; record.reload } # reload here is important

    let(:record) { build(:record) }

    it "assigns some association after commit" do        
      expect{ saving }.to(
        change{ record.some_association_id }.from(nil).to(anything)
      )
    end
  end
end
Epigene
  • 3,634
  • 1
  • 26
  • 31
0

I use something like this

describe 'some method on record' do
  let(:record) { create(:some_record) }
  let(:update_block) { ->(record) { record.save! } } # define an labmda that will be called in a transaction block
  let(:result_method) { :some_method } # define a method to be called
  let(:result) do
    record.class_eval <<~EVAL, __FILE__, __LINE__ + 1
      after_commit :_record_result
       def _record_result
        @_result = public_send(:#{result_method})
      end
    EVAL

    record.transaction do
      update_block.call(record)
    end

    record.instance_variable_get(:'@_result')
  end

  before do
    # apply changes to record
  end

  it 'returns the correct result' do
    expect(result).to eq(some_value)
  end
end
Jose Castellanos
  • 528
  • 4
  • 11