Search code examples
ruby-on-railsruby-on-rails-3rspecobservers

Rails 3: ActiveRecord observer: after_commit callback doesn't fire during tests, but after_save does fire


I have a Rails 3 application. I use after_save callbacks for some models and after_commit callbacks for one of the models. All of the code the code works fine, but during RSpec tests, the after_commit callback doesn't get called when I save a Thing model.

e.g.

class ThingObserver  <  ActiveRecord:Observer
  observe Thing
  def after_commit(thing)
    puts thing.inspect
  end
end

If I change the method name to after_save, it gets called fine during tests. I need to be able to use after_commit for this particular model because in some situations the change to a "thing" happens in a web server, but the effect of the observer happens in a Sidekiq worker, and after_save doesn't guarantee that the data was committed and available when the worker is ready for it.

The config for RSpec looks like this in spec/spec_helper.rb

Rspec.configure do |config|
  #yada yada
  config.use_transactional_fixtures = true
  #yada yada
end

I have also adjusted rake db:create so that it draws from a structure.sql file. In lib/tasks/db.rb

task setup: [ 'test:ensure_environment_is_test', 'db:create', 'db:structure:load', 'db:migrate', 'db:seed' ]

I did this so that I could run tests to ensure that the database was enforcing foreign key constraints.

Is there a way to run both the after_save and after_commit callbacks without making the Rspec use_transactional_fixtures == false?

Or, is there a way to set config.use_transactional_fixtures to 'false' just for that test, or for that test file?


Solution

  • To properly execute database commits you need to enable config.use_transactional_fixtures, but I'd recommend you consider different strategies as that option is disabled by default for the sake of good test design, to enforce your tests to be as unitary and isolated as possible.

    First, you can run ActiveRecord callbacks with #run_callbacks(type), in your case model.run_callbacks(:commit)

    My preferred strategy is to have a method with the logic you want to run, then declare the hook using the method name, then test the method behavior by calling it directly and test that the method is called when the hook is run.

    class Person
      after_commit :register_birth
    
      def register_birth
        # your code
      end
    end
    
    describe Person do
      describe "registering birth" do
        it "registers ..." do
        end
    
        it "runs after database insertion" do
          expect(model).to receive(:register_birth)
          model.run_callbacks(:commit)
        end
      end
    end
    

    That assumes that whatever logic you have on the callback is not essential for the model state, i.e. doesn't change it to something you need to consume right away, and that any other model that interacts with it is indifferent to it. And as such isn't required to run in a test context. That is a powerful design principle that in the long term prevents callbacks to get out of control and generate dependencies for tests by demanding some property unrelated to the unit you are testing to be setup only to be consumed on a callback that you don't care about at that moment.

    But, in the end you know your domain and design requirements better than a stranger so, if you really need the after_commit to run, you can force it with model.run_callbacks(:commit). Just encapsulate that on your factory/fixture and you don't have to remember it every time.