Factory Girl 및 Rspec에서 콜백 건너 뛰기


103

테스트하는 동안 일부 경우에만 실행하고 싶은 애프터 생성 콜백으로 모델을 테스트하고 있습니다. 공장에서 콜백을 건너 뛰거나 실행하려면 어떻게해야합니까?

class User < ActiveRecord::Base
  after_create :run_something
  ...
end

공장:

FactoryGirl.define do
  factory :user do
    first_name "Luiz"
    last_name "Branco"
    ...
    # skip callback

    factory :with_run_something do
      # run callback
  end
end

답변:


111

이것이 최선의 해결책인지 확실하지 않지만 다음을 사용하여 성공적으로 달성했습니다.

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

콜백없이 실행 :

FactoryGirl.create(:user)

콜백으로 실행 :

FactoryGirl.create(:user_with_run_something)

3
:on => :create유효성 검사 를 건너 뛰려면 다음을 사용하십시오.after(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier 2013 년

7
건너 뛰는 콜백 논리를 반전하는 것이 더 낫지 않을까요? 즉, 기본값은 객체를 만들 때 콜백이 트리거된다는 것이어야하며 예외적 인 경우에는 다른 매개 변수를 사용해야합니다. 따라서 FactoryGirl.create (: user)는 콜백을 트리거하는 사용자를 생성해야하며 FactoryGirl.create (: user_without_callbacks)는 콜백없이 사용자를 생성해야합니다. 나는 이것이 단지 "디자인"수정일 뿐이라는 것을 알고 있지만 이것이 기존 코드를 깨뜨리는 것을 피할 수 있고 더 일관성을 유지할 수 있다고 생각합니다.
Gnagno

3
@Minimal의 솔루션이 언급했듯이 Class.skip_callback호출은 다른 테스트에서 지속되므로 다른 테스트에서 콜백이 발생할 것으로 예상하는 경우 건너 뛰는 콜백 논리를 반전하려고하면 실패합니다.
mpdaugherty

나는 after(:build)블록 에서 Mocha로 스터 빙에 대한 @uberllama의 답변을 사용했습니다 . 이렇게하면 공장에서 기본적으로 콜백을 실행하고 사용할 때마다 콜백을 재설정 할 필요가 없습니다.
mpdaugherty

이것이 다른 방식으로 작동한다는 생각이 있습니까? stackoverflow.com/questions/35950470/…
Chris Hough

89

콜백을 실행하지 않으려면 다음을 수행하십시오.

User.skip_callback(:create, :after, :run_something)
Factory.create(:user)

skip_callback은 실행 후 다른 사양에서 지속되므로 다음과 같은 사항을 고려하십시오.

before do
  User.skip_callback(:create, :after, :run_something)
end

after do
  User.set_callback(:create, :after, :run_something)
end

12
콜백을 건너 뛰는 것은 클래스 수준에서 발생하므로 후속 테스트에서 콜백을 계속 건너 뛰기 때문에이 답변이 더 좋습니다.
siannopollo

나도이게 더 좋아. 내 공장이 영구적으로 다르게 작동하는 것을 원하지 않습니다. 특정 테스트 세트를 건너 뛰고 싶습니다.
theUtherSide 2017-10-04

39

이러한 솔루션 중 어느 것도 좋지 않습니다. 클래스가 아닌 인스턴스에서 제거해야하는 기능을 제거하여 클래스를 손상시킵니다.

factory :user do
  before(:create){|user| user.define_singleton_method(:send_welcome_email){}}

콜백을 억제하는 대신 콜백의 기능을 억제합니다. 어떤면에서는이 접근 방식이 더 명시 적이기 때문에 더 좋습니다.


1
나는이 대답을 정말 좋아하고 의도가 즉시 명확하도록 별칭이 지정된 이와 같은 것이 FactoryGirl 자체의 일부 여야하는지 궁금합니다.
Giuseppe

나는 또한이 대답을 너무 좋아해서 다른 모든 것을 반대 투표했지만 콜백이 around_*(예 :)의 종류 인 경우 정의 된 메서드에 블록을 전달해야합니다 user.define_singleton_method(:around_callback_method){|&b| b.call }.
Quv

1
더 나은 솔루션 일뿐만 아니라 어떤 이유로 다른 방법이 저에게 효과적이지 않았습니다. 구현했을 때 콜백 메서드가 없다고 말했지만 생략하면 불필요한 요청을 스텁하도록 요청합니다. 내가 해결책으로 이끌지 만 그 이유를 아는 사람이 있습니까?
Babbz77

27

다른 사용자를 만들 때 after_save 콜백을 더 재사용 할 수 있도록 @luizbranco의 답변을 개선하고 싶습니다.

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

after_save 콜백없이 실행 :

FactoryGirl.create(:user)

after_save 콜백으로 실행 :

FactoryGirl.create(:user, :with_after_save_callback)

내 테스트에서는 사용 된 메서드가 테스트 예제에서 일반적으로 원하지 않는 추가 항목을 실행하기 때문에 기본적으로 콜백없이 사용자를 생성하는 것을 선호합니다.

---------- UPDATE ------------ 테스트 스위트에 불일치 문제가 있었기 때문에 skip_callback 사용을 중단했습니다.

대체 솔루션 1 (스텁 및 스텁 해제 사용) :

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

대체 솔루션 2 (내가 선호하는 접근 방식) :

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

이것이 다른 방식으로 작동한다는 생각이 있습니까? stackoverflow.com/questions/35950470/…
Chris Hough

RuboCop은 대체 솔루션 2에 대해 "스타일 / SingleLineMethods : 단일 행 메서드 정의를 피하십시오"라고 불평하므로 서식을 변경해야하지만 그렇지 않으면 완벽합니다!
coberlin

14

Rails 5- skip_callbackFactoryBot 팩토리에서 건너 뛸 때 인수 오류가 발생합니다.

ArgumentError: After commit callback :whatever_callback has not been defined

rails 5 에서 skip_callback이 인식되지 않는 콜백을 처리하는 방법 이 변경되었습니다 .

ActiveSupport :: Callbacks # skip_callback은 이제 인식 할 수없는 콜백이 제거되면 ArgumentError를 발생시킵니다.

언제 skip_callback 공장에서 호출되면, AR 모델의 실제 콜백이 아직 정의되지 않았습니다.

모든 것을 시도하고 나처럼 머리카락을 뽑았다면 여기에 해결책이 있습니다 (FactoryBot 문제 검색에서 얻었습니다) ( 부품 참고raise: false ).

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

원하는 다른 전략과 함께 자유롭게 사용하십시오.


1
좋아, 이것이 나에게 일어난 일이다. 콜백을 한 번 제거하고 다시 시도하면 이런 일이 발생하므로 한 팩토리에 대해 여러 번 트리거 될 가능성이 높습니다.
slhck

6

이 솔루션은 저에게 효과적이며 Factory 정의에 추가 블록을 추가 할 필요가 없습니다.

user = FactoryGirl.build(:user)
user.send(:create_without_callbacks) # Skip callback

user = FactoryGirl.create(:user)     # Execute callbacks

5

Rspec 3에서 간단한 스텁이 가장 잘 작동했습니다.

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

4
인스턴스 에 대해 설정해야합니다 User. :run_something클래스 메서드가 아닙니다.
PJSCopeland

5
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

중요 사항 당신은 둘 다 지정해야합니다. 이전에만 사용하고 여러 사양을 실행하는 경우 콜백을 여러 번 비활성화하려고 시도합니다. 처음에는 성공하지만 두 번째에는 콜백이 더 이상 정의되지 않습니다. 그래서 오류가 발생합니다.


이로 인해 최근 프로젝트의 제품군에서 일부 난독 화 된 오류가 발생했습니다. @ Sairam의 답변과 비슷한 것이 있지만 콜백은 테스트 사이에 클래스에서 설정되지 않은 상태로 유지되었습니다. 이런.
kfrz

4

내 공장에서 skip_callback을 호출하면 문제가 발생했습니다.

제 경우에는 전체 스택을 테스트 할 때만 실행하려는 생성 전후에 s3 관련 콜백이있는 문서 클래스가 있습니다. 그렇지 않으면 s3 콜백을 건너 뛰고 싶습니다.

내 팩토리에서 skip_callbacks를 시도했을 때 팩토리를 사용하지 않고 문서 객체를 직접 생성 한 경우에도 콜백 스킵이 지속되었습니다. 그래서 대신에 빌드 후 호출에서 모카 스텁을 사용했고 모든 것이 완벽하게 작동합니다.

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

여기에 모든 솔루션 및 공장 내에서 논리를 가지고 들어, 이것은 작동 유일 before_validation후크 (일을하려고 skip_callbackFactoryGirl의의와 before이나 after옵션 buildcreate작동하지 않았다)
마이크 T

3

이것은 현재 rspec 구문 (이 게시물 현재)에서 작동하며 훨씬 더 깔끔합니다.

before do
   User.any_instance.stub :run_something
end

이것은 Rspec 3에서 더 이상 사용되지 않습니다. 일반 스텁을 사용하면 아래의 내 대답을 참조하십시오.
samg 2014

3

before_validation 콜백을 건너 뛰는 방법에 대한 James Chevalier의 답변은 저에게 도움이되지 않았으므로 여기에서 나와 똑같이 straggle하면 작동하는 솔루션입니다.

모델 :

before_validation :run_something, on: :create

공장에서 :

after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }

2
나는 이것을 피하는 것이 바람직하다고 생각합니다. 클래스의 모든 인스턴스에 대해 콜백을 건너 뜁니다 (팩토리 소녀가 생성 한 것뿐만 아니라). 이로 인해 디버그하기 어려울 수있는 일부 사양 실행 문제 (즉, 초기 팩토리 빌드 후 비활성화가 발생하는 경우)가 발생할 수 있습니다. 이것이 사양 / 지원에서 원하는 동작 인 경우 명시 적으로 수행해야합니다. Model.skip_callback(...)
Kevin Sylvestre

2

제 경우에는 내 redis 캐시에 무언가를로드하는 콜백이 있습니다. 하지만 내 테스트 환경에서 redis 인스턴스를 실행하고 싶지 않았습니다.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

내 상황에 대해 위와 비슷하게 load_to_cachespec_helper에서 내 방법을 다음과 같이 스터 빙했습니다.

Redis.stub(:load_to_cache)

또한 이것을 테스트하려는 특정 상황에서 해당 Rspec 테스트 케이스의 이전 블록에서 스텁을 풀면됩니다.

나는 당신이 당신에게 더 복잡한 일이 일어나 after_create거나 이것이 매우 우아하지 않을 수도 있다는 것을 알고 있습니다 . 이 after_create문서 false의 'Canceling callbacks'섹션에 따라 동일한 콜백 및 return을 정의 할 수있는 Factory에서 후크를 정의하여 모델에 정의 된 콜백을 취소 할 수 있습니다 (factory_girl 문서 참조) . (콜백이 실행되는 순서가 확실하지 않으므로이 옵션을 선택하지 않았습니다).

마지막으로 (미안하지만 기사를 찾을 수 없습니다) Ruby를 사용하면 더티 메타 프로그래밍을 사용하여 콜백 후크를 해제 할 수 있습니다 (재설정해야 함). 나는 이것이 가장 덜 선호되는 옵션이라고 생각합니다.

실제로 솔루션이 아닌 한 가지 더 있지만 실제로 개체를 만드는 대신 사양에서 Factory.build를 사용할 수 있는지 확인하십시오. (가능하다면 가장 간단 할 것입니다).


2

위에 게시 된 답변 ( https://stackoverflow.com/a/35562805/2001785) 과 관련하여 공장에 코드를 추가 할 필요가 없습니다. 사양 자체에서 메서드를 오버로드하는 것이 더 쉽다는 것을 알았습니다. 예를 들어 (인용 된 게시물의 공장 코드와 함께) 대신

let(:user) { FactoryGirl.create(:user) }

나는 (인용 된 공장 코드없이) 사용하는 것을 좋아합니다

let(:user) do
  FactoryGirl.build(:user).tap do |u|
      u.define_singleton_method(:send_welcome_email){}
      u.save!
    end
  end
end

이렇게하면 테스트 동작을 이해하기 위해 공장 파일과 테스트 파일을 모두 볼 필요가 없습니다.


1

콜백이 클래스 수준에서 실행 / 설정되기 때문에 다음 솔루션이 더 깨끗한 방법이라는 것을 알았습니다.

# 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

0

다음은 일반적인 방식으로 처리하기 위해 만든 스 니펫입니다.
와 같은 레일 관련 콜백을 포함하여 구성된 모든 콜백을 건너 뛰지 before_save_collection_association만 자동 생성 autosave_associated_records_for_콜백 처럼 ActiveRecord가 제대로 작동하도록하는 데 필요한 일부는 건너 뛰지 않습니다 .

# 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

나중에 :

create(:user, :skip_all_callbacks)

말할 필요도없이 YMMV이므로 테스트 로그에서 실제로 건너 뛰는 것이 무엇인지 살펴보십시오. 정말 필요한 콜백을 추가하는 gem이 있고 테스트가 비참하게 실패하게 만들거나 100 콜백 팻 모델에서 특정 테스트를 위해 몇 가지만 필요합니다. 이러한 경우에는 일시적인:force_callbacks

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

보너스

때로는 유효성 검사를 건너 뛰고 (모두 테스트 속도를 높이기 위해) 다음을 시도해보세요.

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

-1
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

실행하고자 할 때 해당 인스턴스에 대한 트레이 트로 콜백을 설정할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.