ข้ามการโทรกลับใน 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

8
จะไม่เป็นการดีกว่าที่จะเปลี่ยนตรรกะการเรียกกลับแบบข้าม ฉันหมายถึงค่าเริ่มต้นควรเป็นเมื่อฉันสร้างวัตถุการเรียกกลับจะถูกทริกเกอร์และฉันควรใช้พารามิเตอร์อื่นสำหรับกรณีพิเศษ ดังนั้น FactoryGirl.create (: ผู้ใช้) ควรสร้างผู้ใช้ที่เรียกใช้การเรียกกลับและ FactoryGirl.create (: user_without_callbacks) ควรสร้างผู้ใช้โดยไม่มีการเรียกกลับ ฉันรู้ว่านี่เป็นเพียงการปรับเปลี่ยน "การออกแบบ" แต่ฉันคิดว่าวิธีนี้สามารถหลีกเลี่ยงการทำลายโค้ดที่มีอยู่ก่อนและสอดคล้องกันมากขึ้น
Gnagno

3
ตามบันทึกวิธีแก้ปัญหาของ @ Minimal การClass.skip_callbackโทรจะยังคงดำเนินต่อไปในการทดสอบอื่น ๆ ดังนั้นหากการทดสอบอื่น ๆ ของคุณคาดว่าการโทรกลับจะเกิดขึ้นการเรียกกลับจะล้มเหลวหากคุณพยายามกลับตรรกะการข้ามการโทรกลับ
mpdaugherty

ฉันลงเอยด้วยการใช้คำตอบของ @ uberllama เกี่ยวกับการลอกด้วยมอคค่าในafter(:build)บล็อก การดำเนินการนี้ทำให้ค่าเริ่มต้นจากโรงงานของคุณเรียกใช้การโทรกลับและไม่จำเป็นต้องรีเซ็ตการโทรกลับทุกครั้งหลังการใช้งาน
mpdaugherty

คุณมีความคิดเกี่ยวกับการทำงานนี้ในทางอื่นหรือไม่? stackoverflow.com/questions/35950470/…
Chris Hough

90

เมื่อคุณไม่ต้องการเรียกใช้การโทรกลับให้ทำดังต่อไปนี้:

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

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

ฉันต้องการปรับปรุงคำตอบของ @luizbranco เพื่อให้การโทรกลับ after_save ใช้ซ้ำได้มากขึ้นเมื่อสร้างผู้ใช้รายอื่น

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 callback:

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 บ่นด้วย "Style / SingleLineMethods: หลีกเลี่ยงการกำหนดวิธีการแบบบรรทัดเดียว" สำหรับ Alternative Solution 2 ดังนั้นฉันจะต้องเปลี่ยนการจัดรูปแบบ แต่อย่างอื่นก็สมบูรณ์แบบ!
coberlin

15

Rails 5 - skip_callbackเพิ่มข้อผิดพลาด Argument เมื่อข้ามจากโรงงาน FactoryBot

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

วิธีนี้ใช้ได้ผลสำหรับฉันและคุณไม่จำเป็นต้องเพิ่มบล็อกเพิ่มเติมในนิยามโรงงานของคุณ:

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)

5
คุณจะต้องตั้งค่าสำหรับอินสแตนซ์ของ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_callbackกับ FactoryGirl beforeหรือafterตัวเลือกใด ๆสำหรับbuildและcreateไม่ได้ผล)
Mike T

3

สิ่งนี้จะใช้ได้กับไวยากรณ์ rspec ปัจจุบัน (ตามโพสต์นี้) และสะอาดกว่ามาก:

before do
   User.any_instance.stub :run_something
end

สิ่งนี้เลิกใช้แล้วใน Rspec 3 การใช้ต้นขั้วปกติได้ผลสำหรับฉันดูคำตอบของฉันด้านล่าง
samg

3

คำตอบของ James Chevalier เกี่ยวกับวิธีการข้ามการโทรกลับ before_validation ไม่ได้ช่วยฉันดังนั้นหากคุณเบี่ยงเบนเหมือนกับฉันนี่คือวิธีแก้ปัญหา:

ในรุ่น:

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_cacheขีดทับวิธีการใน spec_helper ของฉันด้วย:

Redis.stub(:load_to_cache)

นอกจากนี้ในบางสถานการณ์ที่ฉันต้องการทดสอบสิ่งนี้ฉันต้องยกเลิกการเชื่อมต่อก่อนบล็อกของกรณีทดสอบ Rspec ที่เกี่ยวข้อง

ฉันรู้ว่าคุณอาจมีบางอย่างที่ซับซ้อนกว่านี้เกิดขึ้นในตัวคุณafter_createหรืออาจจะไม่ได้ดูหรูหรามากนัก คุณสามารถพยายามที่จะยกเลิกการเรียกกลับที่กำหนดไว้ในรูปแบบของคุณโดยการกำหนดafter_createเบ็ดในโรงงานของคุณ (โปรดดูเอกสาร factory_girl) ที่คุณอาจจะสามารถกำหนดเรียกกลับเดียวกันและผลตอบแทนfalseตามที่ส่วน 'เรียกกลับยกเลิก' นี้บทความ (ฉันไม่แน่ใจเกี่ยวกับลำดับที่เรียกกลับถูกดำเนินการซึ่งเป็นสาเหตุที่ฉันไม่เลือกตัวเลือกนี้)

สุดท้ายนี้ (ขออภัยฉันไม่พบบทความ) 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แต่จะไม่ข้ามบางอย่างที่จำเป็นเพื่อให้ ActiveRecord ทำงานได้เช่นการautosave_associated_records_for_โทรกลับที่สร้างขึ้นโดยอัตโนมัติ

# 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 ดังนั้นลองดูในบันทึกการทดสอบว่าคุณข้ามอะไรไปบ้าง บางทีคุณอาจมีอัญมณีที่เพิ่มการโทรกลับที่คุณต้องการจริงๆและมันจะทำให้การทดสอบของคุณล้มเหลวอย่างน่าสังเวชหรือจากแบบจำลองไขมันการเรียกกลับ 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.