Lewati panggilan balik di Factory Girl dan Rspec

103

Saya menguji model dengan callback setelah membuat yang saya ingin jalankan hanya pada beberapa kesempatan saat menguji. Bagaimana cara melewati / menjalankan panggilan balik dari pabrik?

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

Pabrik:

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

    factory :with_run_something do
      # run callback
  end
end
luizbranco
sumber

Jawaban:

111

Saya tidak yakin apakah ini solusi terbaik, tetapi saya telah berhasil melakukannya dengan menggunakan:

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

Berjalan tanpa panggilan balik:

FactoryGirl.create(:user)

Berjalan dengan callback:

FactoryGirl.create(:user_with_run_something)
luizbranco
sumber
3
Jika Anda ingin melewatkan :on => :createvalidasi, gunakanafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier
7
bukankah lebih baik untuk membalik logika panggilan balik melewatkan? Maksud saya, defaultnya adalah ketika saya membuat objek callback dipicu, dan saya harus menggunakan parameter yang berbeda untuk kasus luar biasa. jadi FactoryGirl.create (: user) harus membuat pengguna yang memicu callback, dan FactoryGirl.create (: user_without_callbacks) harus membuat pengguna tanpa callback. Saya tahu ini hanyalah modifikasi "desain", tetapi saya pikir ini dapat menghindari kerusakan kode yang sudah ada sebelumnya, dan menjadi lebih konsisten.
Gnagno
3
Seperti yang dicatat oleh solusi @ Minimal, Class.skip_callbackpanggilan tersebut akan tetap ada di seluruh pengujian lainnya, jadi jika pengujian Anda yang lain mengharapkan callback terjadi, pengujian tersebut akan gagal jika Anda mencoba membalik logika callback melewatkan.
mpdaugherty
Saya akhirnya menggunakan jawaban @ uberllama tentang stubbing dengan Mocha di after(:build)blok. Ini memungkinkan default pabrik Anda untuk menjalankan panggilan balik dan tidak perlu menyetel ulang panggilan balik setelah setiap penggunaan.
mpdaugherty
Apakah Anda memiliki pemikiran bahwa ini bekerja sebaliknya? stackoverflow.com/questions/35950470/…
Chris Hough
89

Jika Anda tidak ingin menjalankan callback, lakukan hal berikut:

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

Ketahuilah bahwa skip_callback akan tetap ada di spesifikasi lain setelah dijalankan oleh karena itu pertimbangkan sesuatu seperti berikut:

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

after do
  User.set_callback(:create, :after, :run_something)
end
Minimul
sumber
12
Saya menyukai jawaban ini lebih baik karena secara eksplisit menyatakan bahwa melewatkan callback berkeliaran di tingkat kelas, dan oleh karena itu akan terus melewati callback dalam pengujian berikutnya.
siannopollo
Aku juga lebih suka ini. Saya tidak ingin pabrik saya berperilaku berbeda secara permanen. Saya ingin melewatkannya untuk serangkaian tes tertentu.
theUtherSide
39

Tak satu pun dari solusi ini yang bagus. Mereka merusak kelas dengan menghapus fungsionalitas yang harus dihapus dari instance, bukan dari kelas.

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

Alih-alih menekan callback, saya menyembunyikan fungsionalitas callback. Di satu sisi, saya lebih menyukai pendekatan ini karena lebih eksplisit.

B Tujuh
sumber
1
Saya sangat menyukai jawaban ini, dan bertanya-tanya apakah sesuatu seperti ini, alias sehingga maksudnya segera jelas, harus menjadi bagian dari FactoryGirl itu sendiri.
Giuseppe
Saya juga sangat menyukai jawaban ini sehingga saya akan meremehkan yang lainnya, tetapi tampaknya kita perlu meneruskan blok ke metode yang ditentukan, jika panggilan balik Anda adalah sejenis around_*(misalnya user.define_singleton_method(:around_callback_method){|&b| b.call }).
Quv
1
Tidak hanya solusi yang lebih baik tetapi untuk beberapa alasan metode lain tidak berhasil untuk saya. Ketika saya menerapkannya dikatakan tidak ada metode panggilan balik tetapi ketika saya meninggalkannya, itu akan meminta saya untuk menghentikan permintaan yang tidak perlu. Meskipun ini saya membawa saya ke solusi, apakah ada yang tahu mengapa hal itu mungkin terjadi?
Babbz77
27

Saya ingin menyempurnakan jawaban @luizbranco agar callback after_save lebih dapat digunakan kembali saat membuat pengguna lain.

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

Berjalan tanpa callback after_save:

FactoryGirl.create(:user)

Berjalan dengan callback after_save:

FactoryGirl.create(:user, :with_after_save_callback)

Dalam pengujian saya, saya lebih suka membuat pengguna tanpa callback secara default karena metode yang digunakan menjalankan hal-hal tambahan yang biasanya tidak saya inginkan dalam contoh pengujian saya.

---------- UPDATE ------------ Saya berhenti menggunakan skip_callback karena ada beberapa masalah inkonsistensi dalam rangkaian pengujian.

Solusi Alternatif 1 (penggunaan stub dan 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

Solusi Alternatif 2 (pendekatan pilihan saya):

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
konyak
sumber
Apakah Anda memiliki pemikiran bahwa ini bekerja sebaliknya? stackoverflow.com/questions/35950470/…
Chris Hough
RuboCop mengeluh dengan "Style / SingleLineMethods: Hindari definisi metode baris tunggal" untuk Solusi Alternatif 2, jadi saya perlu mengubah pemformatannya, tetapi sebaliknya sempurna!
coberlin
14

Rails 5 - skip_callbackmemunculkan kesalahan Argument saat melompati dari pabrik FactoryBot.

ArgumentError: After commit callback :whatever_callback has not been defined

Ada perubahan di Rails 5 dengan cara skip_callback menangani callback yang tidak dikenal:

ActiveSupport :: Callbacks # skip_callback sekarang memunculkan ArgumentError jika callback yang tidak dikenal dihapus

Saat skip_callbackdipanggil dari pabrik, callback sebenarnya dalam model AR belum ditentukan.

Jika Anda telah mencoba semuanya dan menarik rambut Anda seperti saya, inilah solusi Anda (dapatkan dari mencari masalah FactoryBot) ( CATATAN raise: falsebagiannya ):

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

Jangan ragu untuk menggunakannya dengan strategi lain apa pun yang Anda sukai.

RudyOnRails
sumber
1
Hebat, inilah yang terjadi pada saya. Perhatikan bahwa jika Anda telah menghapus callback sekali, dan mencobanya lagi, ini terjadi, jadi kemungkinan besar ini akan dipicu beberapa kali untuk pabrik.
slhck
6

Solusi ini berfungsi untuk saya dan Anda tidak perlu menambahkan blok tambahan ke definisi Pabrik Anda:

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

user = FactoryGirl.create(:user)     # Execute callbacks
auralbee.dll
sumber
5

Sebuah rintisan sederhana bekerja paling baik untuk saya di Rspec 3

allow(User).to receive_messages(:run_something => nil)
samg
sumber
4
Anda akan perlu untuk mengaturnya untuk contoh dari User; :run_somethingbukanlah metode kelas.
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

Catatan penting Anda harus menentukan keduanya. Jika hanya digunakan sebelum dan menjalankan beberapa spesifikasi, itu akan mencoba menonaktifkan callback beberapa kali. Ini akan berhasil pertama kali, tetapi yang kedua, panggilan balik tidak akan ditentukan lagi. Jadi itu akan error

AndreiMotinga
sumber
Hal ini menyebabkan beberapa kegagalan yang tidak jelas di suite pada proyek baru-baru ini - Saya memiliki sesuatu yang mirip dengan jawaban @ Sairam tetapi panggilan balik dibiarkan tidak disetel di kelas di antara pengujian. Ups.
kfrz
4

Memanggil skip_callback dari pabrik saya terbukti bermasalah bagi saya.

Dalam kasus saya, saya memiliki kelas dokumen dengan beberapa panggilan balik terkait s3 sebelum dan sesudah membuat yang saya hanya ingin menjalankan ketika menguji tumpukan penuh diperlukan. Jika tidak, saya ingin melewati callback s3 tersebut.

Ketika saya mencoba skip_callbacks di pabrik saya, panggilan balik itu tetap bertahan bahkan ketika saya membuat objek dokumen secara langsung, tanpa menggunakan pabrik. Jadi sebagai gantinya, saya menggunakan mocha stub di panggilan build setelah dan semuanya bekerja dengan sempurna:

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
uberllama
sumber
Dari semua solusi di sini, dan untuk memiliki logika di dalam pabrik, ini adalah satu-satunya yang berfungsi dengan before_validationhook (mencoba melakukan skip_callbackdengan salah satu FactoryGirl beforeatau afteropsi untuk builddan createtidak berfungsi)
Mike T
3

Ini akan bekerja dengan sintaks rspec saat ini (pada posting ini) dan jauh lebih bersih:

before do
   User.any_instance.stub :run_something
end
Zyren
sumber
ini tidak lagi digunakan di Rspec 3. Menggunakan rintisan biasa berhasil untuk saya, lihat jawaban saya di bawah.
samg
3

Jawaban James Chevalier tentang cara melewati panggilan balik before_validation tidak membantu saya jadi jika Anda mengalami hal yang sama seperti saya, berikut ini solusi yang berfungsi:

dalam model:

before_validation :run_something, on: :create

di pabrik:

after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
Tetiana Chupryna
sumber
2
Saya pikir lebih baik menghindari ini. Ini melewatkan callback untuk setiap instance kelas (bukan hanya yang dihasilkan oleh gadis pabrik). Ini akan menyebabkan beberapa masalah eksekusi spesifikasi (yaitu jika penonaktifan terjadi setelah pabrik awal dibangun) yang mungkin sulit untuk di-debug. Jika ini adalah perilaku yang diinginkan dalam spesifikasi / dukungan, itu harus dilakukan secara eksplisit: Model.skip_callback(...)
Kevin Sylvestre
2

Dalam kasus saya, saya memiliki panggilan balik memuat sesuatu ke cache redis saya. Namun kemudian saya tidak memiliki / ingin instance redis berjalan untuk lingkungan pengujian saya.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

Untuk situasi saya, mirip dengan di atas, saya baru saja menghentikan load_to_cachemetode saya di spec_helper saya, dengan:

Redis.stub(:load_to_cache)

Juga, dalam situasi tertentu di mana saya ingin menguji ini, saya hanya perlu melepasnya di blok sebelumnya dari kasus uji Rspec yang sesuai.

Saya tahu Anda mungkin mengalami sesuatu yang lebih rumit dalam diri Anda after_createatau mungkin tidak menganggap ini sangat elegan. Anda dapat mencoba membatalkan callback yang ditentukan dalam model Anda, dengan menentukan after_createhook di Pabrik Anda (lihat dokumen factory_girl), di mana Anda mungkin dapat menentukan callback dan return yang sama false, sesuai dengan bagian 'Membatalkan callback' di artikel ini . (Saya tidak yakin tentang urutan eksekusi callback, itulah sebabnya saya tidak memilih opsi ini).

Terakhir, (maaf saya tidak dapat menemukan artikelnya) Ruby memungkinkan Anda menggunakan beberapa program meta kotor untuk melepaskan kait panggilan balik (Anda harus mengatur ulang). Saya kira ini akan menjadi opsi yang paling tidak disukai.

Ada satu hal lagi, sebenarnya bukan solusi, tetapi lihat apakah Anda bisa lolos dengan Factory.build dalam spesifikasi Anda, daripada benar-benar membuat objek. (Akan menjadi yang paling sederhana jika Anda bisa).

jake
sumber
2

Mengenai jawaban yang diposting di atas, https://stackoverflow.com/a/35562805/2001785 , Anda tidak perlu menambahkan kode ke pabrik. Saya merasa lebih mudah untuk membebani metode dalam spesifikasi itu sendiri. Misalnya, alih-alih (dalam hubungannya dengan kode pabrik di pos yang dikutip)

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

Saya suka menggunakan (tanpa kode pabrik yang dikutip)

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

Dengan cara ini Anda tidak perlu melihat ke pabrik dan file pengujian untuk memahami perilaku pengujian.

bhfailor
sumber
1

Saya menemukan solusi berikut menjadi cara yang lebih bersih karena callback dijalankan / disetel pada tingkat kelas.

# 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
Sairam
sumber
0

Berikut cuplikan yang saya buat untuk menangani ini dengan cara yang umum.
Ini akan melewati setiap callback yang dikonfigurasi, termasuk callback yang berhubungan dengan rails before_save_collection_association, tetapi tidak akan melewatkan beberapa hal yang diperlukan untuk membuat ActiveRecord berfungsi dengan baik, seperti autosave_associated_records_for_callback yang dibuat secara otomatis .

# 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

lalu nanti:

create(:user, :skip_all_callbacks)

Tentu saja, YMMV, jadi lihat di log pengujian apa yang sebenarnya Anda lewati. Mungkin Anda memiliki permata yang menambahkan panggilan balik yang benar-benar Anda butuhkan dan itu akan membuat pengujian Anda gagal total atau dari 100 model gemuk panggilan balik Anda, Anda hanya perlu pasangan untuk tes tertentu. Untuk kasus tersebut, coba transient:force_callbacks

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

BONUS

Terkadang Anda juga perlu melewati validasi (semua dalam upaya untuk membuat tes lebih cepat), lalu coba dengan:

  trait :skip_validate do
    to_create { |instance| instance.save(validate: false) }
  end
Alter Lagos
sumber
-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

Anda bisa menyetel callback dengan sifat untuk instance tersebut saat Anda ingin menjalankannya.

pengguna6520080
sumber