Question

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.

Was it helpful?

Solution 3

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.

OTHER TIPS

Assuming you're on Rails 4.1.4 or newer:

User.no_touching do
  Post.last.save
end

or even

ActiveRecord::Base.no_touching do
  Post.last.save
end

See ActiveRecord::NoTouching.

I disagree with the notion of altering the code for test purposes. Testing from my point of view should be an independent procedure.

As I see it you should provide a way to your test suite to alter the behavior of a model only for certain cases.

The following code

class Book < ActiveRecord::Base
  belongs_to :author, touch: true
end

class Author < ActiveRecord::Base
  has_many :books
end

which is your case will define an instance method

belongs_to_touch_after_save_or_destroy_for_author

behind the scene for the Book class. ( thanks to AR http://apidock.com/rails/ActiveRecord/Associations/Builder/BelongsTo/add_touch_callbacks )

So in your test code you could override that method to do something different or nothing at all!

In my case I use Rspec with FactoryGirl, so what I did was to create a special factory trait for the Book class which redefines belongs_to_touch_after_save_or_destroy_for_author for that object

FactoryGirl.define do
  factory :book do
    ...
    ...
  end

  trait :no_touch do
    before(:create) do |book_no_touch|
      def book_no_touch.belongs_to_touch_after_save_or_destroy_for_author
        true
      end
    end
  end
end

That way, when you need to test something where touching the related objects is irrelevant, you can create a book object with that factory

book = FactoryGirl.create(:book, :no_touch)

I'm not sure if this is going to work but you could try the following:

belongs_to :foo, touch: APP_CONFIG['doll_touch']

where APP_CONFIG is an application parameter that is set following this guide.

So, in your production/development part of the configuration, you set doll_touch to true and in your test to false.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top