Search code examples
ruby-on-railsrspecassociationsmetaprogrammingbelongs-to

How to disable belongs_to :touch option in Rspec tests for Rails models?


Having a large model stack and using doll caching techniques extensively, one ends up with lots of parent models been "touched" after a model update.

While testing, this seems to be a time waster unless you try to test that feature specifically.

Is there a way to prevent models to touch their belongs_to associations for the test environment or at a test level?

UPDATE 1:

My first attempt to the case would be to

# /config/initializers/extensions.rb
#
class ActiveRecord::Base
  def self.without_touch_for_association(association_name, &block)
    association_name = association_name.to_sym
    association = self.reflect_on_all_associations(:belongs_to).select { |reflection| reflection.name == association_name }.first
    options = association.options
    association.instance_variable_set :@options, options.except(:touch)

    yield

    association.instance_variable_set :@options, options
  end
end

Post.without_touch_for_association(:user) do
  Post.last.save
end

Of course, no success and saving Post.last still touches it's User.

UPDATING RATIONALE:

I understand and agree that this approach may be a source of bugs and it's not a good practice at all. The thing is that I have a huge suite with lots of both integration and unit tests. Doll caching also gets deep in the model tree. Every time I look at the logs, I see a significant % of touch-related queries. I know the best way would be optimizing the unit tests to add more mocking and stubbing and less persistence. Solving the issue within integration tests is more difficult.

In any case, I'm asking this question for the sake of learning and research. I am interested in exploring the potential speed improvements of this technique.

SOLUTION: see my own answer below for the working code.


Solution

  • For Rails >= 4.2

    Thanks to @Dorian, in Rails 4.2 the way to go is using ActiveRecord::NoTouching.

    For Rails < 4.2

    My working code in rspec support file:

    # /spec/support/active_record_extensions.rb
    class ActiveRecord::Base
      def self.without_touch_for_association(association, &block)
        method_name = :"belongs_to_touch_after_save_or_destroy_for_#{association}"
        return unless self.instance_methods.include?(method_name)
    
        method = self.send(:instance_method, method_name)
        self.send(:define_method, method_name) { true }
    
        yield
    
        self.send(:define_method, method_name, method)
        nil
      end
    
      def self.disable_touch_associations!
        associations = self.reflect_on_all_associations(:belongs_to)
        associations.each do |association|
          self.without_touch_for_association association.name do
            return
          end
        end
        nil
      end
    end
    

    Add this to your ./spec/spec_helper.rb to disable all touch calls for any model defined, for the whole test suite:

    RSpec.configure do |config|
      if ENV['SILENCE_TOUCHES']
        config.before :suite do
          ActiveRecord::Base.descendants.each {|model| model.disable_touch_associations! }
        end
      end
    end
    

    Temporarely disabling a touch for a model and association in a particular test.

    Post.without_touch_for_association(:user) do
      Post.last.save
    end
    

    Thanks to @xlembouras below for pointing me to the right direction!

    I'm playing with this feature on our tests and I'm noticing a 25% reduction in test suite speed, for a 30min test suite. I may post more accurate results after more thorough research.