ChatGPT解决这个技术问题 Extra ChatGPT

Skip callbacks on Factory Girl and Rspec

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

l
luizbranco

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)

If you want to skip an :on => :create validation, use after(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
wouldn't it be better to invert the skipping callback logic? I mean, the default should be that when I create an object the callbacks are triggered, and I should use a different parameter for the exceptional case. so FactoryGirl.create(:user) should create the user triggering the callbacks, and FactoryGirl.create(:user_without_callbacks) should create the user without the callbacks. I know this is just a "design" modification, but I think this can avoid to break pre existing code, and be more consistent.
As @Minimal's solution notes, the Class.skip_callback call will be persistent across other tests, so if your other tests expect the callback to occur, they will fail if you try to invert the skipping callback logic.
I ended up using @uberllama's answer about stubbing with Mocha in the after(:build) block. This lets your factory default to running the callback and doesn't require resetting the callback after every usage.
Do you have any thoughts of this working the other way? stackoverflow.com/questions/35950470/…
M
Minimul

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

I like this answer better because it explicitly states that skipping callbacks hangs around on the class level, and therefore would continue to skip callbacks in subsequent tests.
I like this better too. I don't want my factory to permanently behave differently. I want to skip it for a particular set of tests.
C
Code-Apprentice

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){}}
end

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.


I really like this answer, and wonder if something like this, aliased so that the intent is immediately clear, should be part of FactoryGirl itself.
I also like this answer so much I'd downvote everything else, but it appears we need to pass a block to the defined method, if it's your callback is the kindred of around_* (e.g. user.define_singleton_method(:around_callback_method){|&b| b.call }).
Not only a better solution but for some reason the other method didn't work for me. When I implemented it it said no callback method existed but when I left it out it would ask me to stub the unnecessary requests. Although it l lead me to a solution does anyone know why that might be?
k
konyak

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

Do you have any thoughts of this working the other way? stackoverflow.com/questions/35950470/…
RuboCop complains with "Style/SingleLineMethods: Avoid single-line method definitions" for Alternative Solution 2, so I'll need to change the formatting, but otherwise it's perfect!
R
RudyOnRails

Rails 5 - skip_callback raising Argument error when skipping from a FactoryBot factory.

ArgumentError: After commit callback :whatever_callback has not been defined

There was a change in Rails 5 with how skip_callback handles unrecognized callbacks:

ActiveSupport::Callbacks#skip_callback now raises an ArgumentError if an unrecognized callback is remove

When skip_callback is called from the factory, the real callback in the AR model is not yet defined.

If you've tried everything and pulled your hair out like me, here is your solution (got it from searching FactoryBot issues) (NOTE the raise: false part):

after(:build) { YourSweetModel.skip_callback(:commit, :after, :whatever_callback, raise: false) }

Feel free to use it with whatever other strategies you prefer.


Great, this is exactly what happened to me. Note that if you've removed a callback once, and try it again, this happens, so it's quite likely that this will be triggered multiple times for a factory.
a
auralbee

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

R
Ramesh Chandra

A simple stub worked best for me in Rspec 3

allow_any_instance_of(User).to receive_messages(:run_something => nil)

You'd need to set it up for instances of User; :run_something isn't a class method.
A
AndreiMotinga
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


This caused some obfuscated failures in a suite on a recent project - I had something similar to @Sairam's answer but the callback was being left unset in the class between tests. Whoops.
u
uberllama

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

Of all the solutions here, and for having the logic within the factory, this is the only one that works with a before_validation hook (trying to do skip_callback with any of FactoryGirl's before or after options for build and create didn't work)
Z
Zyren

This will work with current rspec syntax (as of this post) and is much cleaner:

before do
   User.any_instance.stub :run_something
end

this is deprecated in Rspec 3. Using a regular stub worked for me, see my answer below.
T
Tetiana Chupryna

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) }

I think it is preferable to avoid this. It skips callbacks for every instance of the class (not just the ones generated by factory girl). This'll lead to some spec execution issues (i.e. if the disable happens after the initial factory is build) that can be difficult to debug. If this is the desired behaviour in the spec/support it should be done explicitly: Model.skip_callback(...)
J
James N

This is an older question, with some good answers, but none of them quite worked for me for a few reasons

didn't like the idea of modifying the behavior of some class at runtime

didn't want to use attr_accessor all throughout my classes because it seemed weird to put logic only used for tests inside models

didn't want to put a call to rspec before/after blocks on various specs to stub/unstub behavior

using FactoryBot you can use transient in your factory to set a switch to modify behavior of your classes. As a result, factories/specs look like

#factory
FactoryBot.define do
  factory :user do
    
    transient do
      skip_after_callbacks { true }
    end

    after(:build) do |user, evaluator|
      if evaluator.skip_after_callbacks
        class << user
          def callback_method1; true; end
          def callback_method2; true; end
          def callback_method3; true; end
        end
      end
    end
  end
end

# without running callbacks
user = create(:user)
# with running callbacks for certain specs
user = create(:user, skip_after_callbacks: false)

This worked for me because our app has certain methods that are triggered as a result of various after_create/after_commit callbacks that run to external services, so by default I don't typically need those to run in specs. Doing this saved our test suite on various calls using VCR. YMMV


t
tmr08c

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).


S
Sairam

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

b
bhfailor

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.


A
Alter Lagos

Here's a snippet I created to handle this in a generic way.
It will skip every callback configured, including rails-related callbacks like before_save_collection_association, but it won't skip some needed to make ActiveRecord work ok, like autogenerated autosave_associated_records_for_ callbacks.

# In some factories/generic_traits.rb file or something like that
FactoryBot.define do
  trait :skip_all_callbacks do
    transient do
      force_callbacks { [] }
    end

    after(:build) do |instance, evaluator|
      klass = instance.class
      # I think with these callback types should be enough, but for a full
      # list, check `ActiveRecord::Callbacks::CALLBACKS`
      %i[commit create destroy save touch update].each do |type|
        callbacks = klass.send("_#{type}_callbacks")
        next if callbacks.empty?

        callbacks.each do |cb|
          # Autogenerated ActiveRecord after_create/after_update callbacks like
          # `autosave_associated_records_for_xxxx` won't be skipped, also
          # before_destroy callbacks with a number like 70351699301300 (maybe
          # an Object ID?, no idea)
          next if cb.filter.to_s =~ /(autosave_associated|\d+)/

          cb_name = "#{klass}.#{cb.kind}_#{type}(:#{cb.filter})"
          if evaluator.force_callbacks.include?(cb.filter)
            next Rails.logger.debug "Forcing #{cb_name} callback"
          end

          Rails.logger.debug "Skipping #{cb_name} callback"
          instance.define_singleton_method(cb.filter) {}
        end
      end
    end
  end
end

then later:

create(:user, :skip_all_callbacks)

Needless to say, YMMV, so take a look in the test logs what are you really skipping. Maybe you have a gem adding a callback you really need and it will make your tests to fail miserably or from your 100 callbacks fat model you just need a couple for a specific test. For those cases, try the transient :force_callbacks

create(:user, :skip_all_callbacks, force_callbacks: [:some_important_callback])

BONUS

Sometimes you need also skip validations (all in a effort to make tests faster), then try with:

  trait :skip_validate do
    to_create { |instance| instance.save(validate: false) }
  end

M
Mehmet Adil İstikbal

I had a familiar problem that i wanted to skip callbacks only when i create a record from FactoryBot and answers posted here did not solve my problem so i found my own solution, i'm posting it here so may be it will be useful for someone else.

Class

class User < ApplicationRecord
  before_save :verify
end

Factory

FactoryBot.define do
  factory :user do
    transient do
      skip_verify_callback { true }
    end
    
    before(:create) do |user, evaluator|
      user.class.skip_callback(:save, :before, :verify) if evaluator.skip_verify_callback
    end

    after(:create) do |user, evaluator|
      user.class.set_callback(:save, :before, :verify) if evaluator.skip_verify_callback
    end
  end
end

NOTE: Above create callbacks runs after only FactoryBot.create, so FactoryBot.build will not trigger these.

I set the default behavior of the factory to skip the verify callback while i still have the ability to prevent this by creating user with a argument like this:

FactoryBot.create(:user, skip_verify_callback: false)

I think this approach safer because FactoryBot.create starts and ends in instant and we won't have any side effects of skipping callbacks.


u
user6520080
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.


关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now