问题
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.
回答1:
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.
回答2:
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)
回答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.
回答4:
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
.
来源:https://stackoverflow.com/questions/18922864/how-to-disable-belongs-to-touch-option-in-rspec-tests-for-rails-models