可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm testing a model with an after create callback that I'd like to run only on some occasions while testing. How can I skip/run callbacks from a factory?
class User < ActiveRecord::Base
after_create :run_something
...
end
Factory:
FactoryGirl.define do
factory :user do
first_name "Luiz"
last_name "Branco"
...
# skip callback
factory :with_run_something do
# run callback
end
end
回答1:
I'm not sure if it is the best solution, but I have successfully achieved this using:
FactoryGirl.define do
factory :user do
first_name "Luiz"
last_name "Branco"
#...
after(:build) { |user| user.class.skip_callback(:create, :after, :run_something) }
factory :user_with_run_something do
after(:create) { |user| user.send(:run_something) }
end
end
end
Running without callback:
FactoryGirl.create(:user)
Running with callback:
FactoryGirl.create(:user_with_run_something)
回答2:
When you don't want to run a callback do the following:
User.skip_callback(:create, :after, :run_something)
Factory.create(:user)
Be aware that skip_callback will be persistant across other specs after it is run therefore consider something like the following:
before do
User.skip_callback(:create, :after, :run_something)
end
after do
User.set_callback(:create, :after, :run_something)
end
回答3:
I'd like to make an improvement to @luizbranco 's answer to make after_save callback more reusable when creating other users.
FactoryGirl.define do
factory :user do
first_name "Luiz"
last_name "Branco"
#...
after(:build) { |user|
user.class.skip_callback(:create,
:after,
:run_something1,
:run_something2)
}
trait :with_after_save_callback do
after(:build) { |user|
user.class.set_callback(:create,
:after,
:run_something1,
:run_something2)
}
end
end
end
Running without after_save callback:
FactoryGirl.create(:user)
Running with after_save callback:
FactoryGirl.create(:user, :with_after_save_callback)
In my test, I prefer to create users without the callback by default because the methods used run extra stuff I don't normally want in my test examples.
----------UPDATE------------
I stopped using skip_callback because there were some inconsistency issues in the test suite.
Alternative Solution 1 (use of stub and unstub):
after(:build) { |user|
user.class.any_instance.stub(:run_something1)
user.class.any_instance.stub(:run_something2)
}
trait :with_after_save_callback do
after(:build) { |user|
user.class.any_instance.unstub(:run_something1)
user.class.any_instance.unstub(:run_something2)
}
end
Alternative Solution 2 (my preferred approach):
after(:build) { |user|
class << user
def run_something1; true; end
def run_something2; true; end
end
}
trait :with_after_save_callback do
after(:build) { |user|
class << user
def run_something1; super; end
def run_something2; super; end
end
}
end
回答4:
None of these solutions are good. They deface the class by removing functionality that should be removed from the instance, not from the class.
factory :user do
before(:create){|user| user.define_singleton_method(:send_welcome_email){}}
Instead of suppressing the callback, I am suppressing the functionality of the callback. In a way, I like this approach better because it is more explicit.
回答5:
This solution works for me and you don´t have to add an additional block to your Factory definition:
user = FactoryGirl.build(:user)
user.send(:create_without_callbacks) # Skip callback
user = FactoryGirl.create(:user) # Execute callbacks
回答6:
A simple stub worked best for me in Rspec 3
allow(User).to receive_messages(:run_something => nil)
回答7:
Calling skip_callback from my factory proved problematic for me.
In my case, I have a document class with some s3-related callbacks in before and after create that I only want to run when testing the full stack is necessary. Otherwise, I want to skip those s3 callbacks.
When I tried skip_callbacks in my factory, it persisted that callback skip even when I created a document object directly, without using a factory. So instead, I used mocha stubs in the after build call and everything is working perfectly:
factory :document do
upload_file_name "file.txt"
upload_content_type "text/plain"
upload_file_size 1.kilobyte
after(:build) do |document|
document.stubs(:name_of_before_create_method).returns(true)
document.stubs(:name_of_after_create_method).returns(true)
end
end
回答8:
This will work with current rspec syntax (as of this post) and is much cleaner:
before do
User.any_instance.stub :run_something
end
回答9:
James Chevalier's answer about how to skip before_validation callback didn't help me so if you straggle the same as me here is working solution:
in model:
before_validation :run_something, on: :create
in factory:
after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
回答10:
FactoryGirl.define do
factory :order, class: Spree::Order do
trait :without_callbacks do
after(:build) do |order|
order.class.skip_callback :save, :before, :update_status!
end
after(:create) do |order|
order.class.set_callback :save, :before, :update_status!
end
end
end
end
Important note you should specify both of them.
If only use before and run multiple specs, it'll try to disable callback multiple times. It'll succeed the first time, but on the second, callback isn't going to be defined anymore. So it'll error out
回答11:
In my case I have the callback loading something to my redis cache. But then I did not have/want a redis instance running for my test environment.
after_create :load_to_cache
def load_to_cache
Redis.load_to_cache
end
For my situation, similar to above, I just stubbed my load_to_cache
method in my spec_helper,
with:
Redis.stub(:load_to_cache)
Also, in certain situation where I want to the test this, I just have to unstub them in the before block of the corresponding Rspec test cases.
I know you might have something more complicated happening in your after_create
or might not find this very elegant. You can try to cancel the callback defined in your model, by defining an after_create
hook in your Factory (refer to factory_girl docs), where you can probably define a the same callback and return false
, according to the 'Canceling callbacks' section of this article. (I am unsure about order in which callback are executed, which is why I didn't go for this option).
Lastly, (sorry I am not able to find the article) Ruby allows you to use some dirty meta programming to unhook a callback hook (you will have to reset it). I guess this would be the least preferred option.
Well there is one more thing, not really a solution, but see if you can get away with Factory.build in your specs, instead of actually creating the object. (Would be the simplest if you can).
回答12:
Regarding the answer posted above, https://stackoverflow.com/a/35562805/2001785, you do not need to add the code to the factory. I found it easier to overload the methods in the specs themselves. For example, instead of (in conjunction with the factory code in the cited post)
let(:user) { FactoryGirl.create(:user) }
I like using (without the cited factory code)
let(:user) do
FactoryGirl.build(:user).tap do |u|
u.define_singleton_method(:send_welcome_email){}
u.save!
end
end
end
This way you do not need to look at both the factory and the test files to understand the behavior of the test.
回答13:
I found the following solution to be a cleaner way since the callback is run/set at a class level.
# create(:user) - will skip the callback.
# create(:user, skip_create_callback: false) - will set the callback
FactoryBot.define do
factory :user do
first_name "Luiz"
last_name "Branco"
transient do
skip_create_callback true
end
after(:build) do |user, evaluator|
if evaluator.skip_create_callback
user.class.skip_callback(:create, :after, :run_something)
else
user.class.set_callback(:create, :after, :run_something)
end
end
end
end
回答14:
FactoryGirl.define do
factory :user do
first_name "Luiz"
last_name "Branco"
#...
after(:build) { |user| user.class.skip_callback(:create, :after, :run_something) }
trait :user_with_run_something do
after(:create) { |user| user.class.set_callback(:create, :after, :run_something) }
end
end
end
You could just set the callback with a trait for those instances when you want run it.