Bỏ qua các lệnh gọi lại trên Factory Girl và Rspec


103

Tôi đang thử nghiệm một mô hình với lệnh gọi lại sau khi tạo mà tôi chỉ muốn chạy trong một số trường hợp trong khi thử nghiệm. Làm cách nào để bỏ qua / chạy lệnh gọi lại từ nhà máy?

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

Nhà máy:

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

    factory :with_run_something do
      # run callback
  end
end

Câu trả lời:


111

Tôi không chắc đó có phải là giải pháp tốt nhất hay không, nhưng tôi đã đạt được điều này bằng cách sử dụng:

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

Chạy mà không cần gọi lại:

FactoryGirl.create(:user)

Chạy với cuộc gọi lại:

FactoryGirl.create(:user_with_run_something)

3
Nếu bạn muốn bỏ qua :on => :createxác thực, hãy sử dụngafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier

7
Sẽ tốt hơn nếu đảo ngược logic gọi lại bỏ qua? Ý tôi là, mặc định phải là khi tôi tạo một đối tượng, các lệnh gọi lại được kích hoạt và tôi nên sử dụng một tham số khác cho trường hợp ngoại lệ. vì vậy FactoryGirl.create (: user) nên tạo người dùng kích hoạt các lệnh gọi lại và FactoryGirl.create (: user_without_callbacks) nên tạo người dùng không có lệnh gọi lại. Tôi biết đây chỉ là một sửa đổi "thiết kế", nhưng tôi nghĩ điều này có thể tránh được việc phá vỡ mã hiện có trước đó và nhất quán hơn.
Gnagno

3
Như giải pháp của @ Minimal lưu ý, Class.skip_callbackcuộc gọi sẽ liên tục trong các thử nghiệm khác, vì vậy nếu các thử nghiệm khác của bạn mong đợi việc gọi lại xảy ra, chúng sẽ không thành công nếu bạn cố gắng đảo ngược logic gọi lại đang bỏ qua.
mpdaugherty

Tôi đã kết thúc bằng cách sử dụng câu trả lời của @ uberllama về việc khai thác với Mocha trong after(:build)khối. Điều này cho phép nhà máy của bạn mặc định chạy lệnh gọi lại và không yêu cầu đặt lại lệnh gọi lại sau mỗi lần sử dụng.
mpdaugherty

Bạn có bất kỳ suy nghĩ về việc này làm việc theo cách khác? stackoverflow.com/questions/35950470/…
Chris Hough

89

Khi bạn không muốn gọi lại, hãy làm như sau:

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

Hãy lưu ý rằng jump_callback sẽ tồn tại trên các thông số kỹ thuật khác sau khi nó được chạy, do đó, hãy xem xét một số thứ như sau:

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

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

12
Tôi thích câu trả lời này hơn vì nó tuyên bố rõ ràng rằng việc bỏ qua lệnh gọi lại xảy ra xung quanh ở cấp lớp và do đó sẽ tiếp tục bỏ qua lệnh gọi lại trong các bài kiểm tra tiếp theo.
siannopollo

Tôi cũng thích điều này hơn. Tôi không muốn nhà máy của mình vĩnh viễn hoạt động khác thường. Tôi muốn bỏ qua nó cho một bộ kiểm tra cụ thể.
theUtherSide

39

Không có giải pháp nào trong số này là tốt. Chúng khử mặt lớp bằng cách loại bỏ chức năng cần được loại bỏ khỏi cá thể, không phải khỏi lớp.

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

Thay vì ngăn chặn cuộc gọi lại, tôi đang ngăn chặn chức năng của cuộc gọi lại. Theo một cách nào đó, tôi thích cách tiếp cận này hơn vì nó rõ ràng hơn.


1
Tôi thực sự thích câu trả lời này và tự hỏi liệu thứ gì đó như thế này, được đặt bí danh để mục đích rõ ràng ngay lập tức, có nên là một phần của chính FactoryGirl hay không.
Giuseppe

Tôi cũng thích câu trả lời này đến nỗi tôi sẽ phản đối mọi thứ khác, nhưng có vẻ như chúng ta cần chuyển một khối cho phương thức đã xác định, nếu đó là lệnh gọi lại của bạn thuộc loại around_*(ví dụ user.define_singleton_method(:around_callback_method){|&b| b.call }).
Quv

1
Không chỉ một giải pháp tốt hơn mà vì một số lý do mà phương pháp khác không hiệu quả với tôi. Khi tôi triển khai nó, nó nói rằng không có phương thức gọi lại nào tồn tại nhưng khi tôi bỏ nó đi, nó sẽ yêu cầu tôi khai báo các yêu cầu không cần thiết. Mặc dù nó dẫn tôi đến một giải pháp nhưng có ai biết tại sao lại như vậy không?
Babbz77

27

Tôi muốn cải thiện câu trả lời của @luizbranco để làm cho lệnh gọi lại after_save có thể tái sử dụng nhiều hơn khi tạo người dùng khác.

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

Chạy mà không có lệnh gọi lại after_save:

FactoryGirl.create(:user)

Chạy với lệnh gọi lại after_save:

FactoryGirl.create(:user, :with_after_save_callback)

Trong thử nghiệm của mình, tôi muốn tạo người dùng không có lệnh gọi lại theo mặc định vì các phương thức được sử dụng chạy thêm những thứ mà tôi thường không muốn trong các ví dụ thử nghiệm của mình.

---------- CẬP NHẬT ------------ Tôi đã ngừng sử dụng jump_callback vì có một số vấn đề không nhất quán trong bộ thử nghiệm.

Giải pháp thay thế 1 (sử dụng phần gốc và phần tách):

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

Giải pháp thay thế 2 (cách tiếp cận ưa thích của tôi):

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

Bạn có bất kỳ suy nghĩ về việc này làm việc theo cách khác? stackoverflow.com/questions/35950470/…
Chris Hough

RuboCop phàn nàn với "Style / SingleLineMethods: Tránh định nghĩa phương thức một dòng" cho Giải pháp thay thế 2, vì vậy tôi sẽ cần phải thay đổi định dạng, nhưng nếu không thì nó hoàn hảo!
coberlin 23/02/18

14

Rails 5 - skip_callbacktăng lỗi Đối số khi bỏ qua từ nhà máy FactoryBot.

ArgumentError: After commit callback :whatever_callback has not been defined

Có một sự thay đổi trong Rails 5 với cách bỏ qua_callback xử lý các lệnh gọi lại không được công nhận:

ActiveSupport :: Callbacks # ignore_callback hiện tăng ArgumentError nếu lệnh gọi lại không được công nhận bị xóa

Khi nào skip_callbackđược gọi từ nhà máy, lệnh gọi lại thực sự trong mô hình AR vẫn chưa được xác định.

Nếu bạn đã thử mọi cách và nhổ tóc như tôi, đây là giải pháp của bạn (có được từ việc tìm kiếm các sự cố FactoryBot) ( LƯU Ý raise: falsephần ):

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

Hãy thoải mái sử dụng nó với bất kỳ chiến lược nào khác mà bạn muốn.


1
Tuyệt vời, đây chính xác là những gì đã xảy ra với tôi. Lưu ý rằng nếu bạn đã xóa lệnh gọi lại một lần và thử lại, điều này sẽ xảy ra, vì vậy rất có thể điều này sẽ được kích hoạt nhiều lần cho một nhà máy.
slhck

6

Giải pháp này phù hợp với tôi và bạn không phải thêm một khối bổ sung vào định nghĩa Nhà máy của mình:

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

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

5

Một sơ khai đơn giản phù hợp nhất với tôi trong Rspec 3

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

4
Bạn cần phải thiết lập nó cho các trường hợp của User; :run_somethingkhông phải là một phương thức lớp.
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

Lưu ý quan trọng bạn nên chỉ định cả hai. Nếu chỉ sử dụng trước đó và chạy nhiều thông số kỹ thuật, nó sẽ cố gắng tắt gọi lại nhiều lần. Nó sẽ thành công trong lần đầu tiên, nhưng vào lần thứ hai, callback sẽ không được xác định nữa. Vì vậy, nó sẽ lỗi


Điều này gây ra một số lỗi khó hiểu trong một bộ trong một dự án gần đây - tôi có một cái gì đó tương tự với câu trả lời của @ Sairam nhưng cuộc gọi lại đang được không đặt trong lớp giữa các lần kiểm tra. Rất tiếc.
kfrz

4

Việc gọi bỏ qua_callback từ nhà máy của tôi tỏ ra có vấn đề với tôi.

Trong trường hợp của tôi, tôi có một lớp tài liệu với một số lệnh gọi lại liên quan đến s3 trước và sau khi tạo mà tôi chỉ muốn chạy khi kiểm tra toàn bộ ngăn xếp là cần thiết. Nếu không, tôi muốn bỏ qua các lệnh gọi lại s3 đó.

Khi tôi thử bỏ qua_callbacks trong nhà máy của mình, nó vẫn tồn tại rằng lệnh gọi lại bỏ qua ngay cả khi tôi tạo đối tượng tài liệu trực tiếp mà không cần sử dụng nhà máy. Vì vậy, thay vào đó, tôi đã sử dụng sơ khai mocha trong lệnh gọi sau khi xây dựng và mọi thứ đang hoạt động hoàn hảo:

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

Trong tất cả các giải pháp ở đây, và vì có logic trong nhà máy, đây là duy nhất làm việc với một before_validationcái móc (cố gắng làm skip_callbackvới bất kỳ FactoryGirl của beforehoặc aftertùy chọn cho buildcreatekhông làm việc)
Mike T

3

Điều này sẽ hoạt động với cú pháp rspec hiện tại (tính đến bài đăng này) và gọn gàng hơn nhiều:

before do
   User.any_instance.stub :run_something
end

điều này không được chấp nhận trong Rspec 3. Sử dụng sơ khai thông thường phù hợp với tôi, hãy xem câu trả lời của tôi bên dưới.
samg

3

Câu trả lời của James Chevalier về cách bỏ qua lệnh gọi lại before_validation không giúp ích được gì cho tôi, vì vậy nếu bạn phân vân giống tôi thì đây là giải pháp hiệu quả:

trong mô hình:

before_validation :run_something, on: :create

trong nhà máy:

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

2
Tôi nghĩ tốt hơn là nên tránh điều này. Nó bỏ qua các lệnh gọi lại cho mọi phiên bản của lớp (không chỉ những phiên bản được tạo bởi factory girl). Điều này sẽ dẫn đến một số vấn đề thực thi thông số kỹ thuật (tức là nếu việc vô hiệu hóa xảy ra sau khi xây dựng nhà máy ban đầu) có thể khó gỡ lỗi. Nếu đây là hành vi mong muốn trong spec / hỗ trợ nó nên được thực hiện một cách rõ ràng: Model.skip_callback(...)
Kevin Sylvestre

2

Trong trường hợp của tôi, tôi có lệnh gọi lại đang tải một thứ gì đó vào bộ nhớ cache redis của tôi. Nhưng sau đó tôi không có / muốn một phiên bản redis chạy cho môi trường thử nghiệm của tôi.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

Đối với tình huống của tôi, tương tự như trên, tôi chỉ đặt load_to_cachephương thức của mình trong spec_helper của tôi, với:

Redis.stub(:load_to_cache)

Ngoài ra, trong một số tình huống nhất định khi tôi muốn kiểm tra điều này, tôi chỉ cần bỏ đăng chúng trong khối trước của các trường hợp kiểm tra Rspec tương ứng.

Tôi biết bạn có thể có một cái gì đó phức tạp hơn xảy ra trong của bạn after_createhoặc có thể không thấy điều này rất thanh lịch. Bạn có thể cố gắng hủy lệnh gọi lại được xác định trong mô hình của mình, bằng cách xác định một after_createhook trong Factory của bạn (tham khảo tài liệu factory_girl), nơi bạn có thể xác định một lệnh gọi lại và trả về giống nhau false, theo phần 'Hủy lệnh gọi lại' của bài viết này . (Tôi không chắc chắn về thứ tự mà lệnh gọi lại được thực hiện, đó là lý do tại sao tôi không chọn tùy chọn này).

Cuối cùng, (xin lỗi tôi không thể tìm thấy bài viết) Ruby cho phép bạn sử dụng một số lập trình meta bẩn để tháo móc gọi lại (bạn sẽ phải đặt lại nó). Tôi đoán đây sẽ là lựa chọn ít được ưu tiên nhất.

Còn một điều nữa, không hẳn là một giải pháp, nhưng hãy xem liệu bạn có thể sử dụng Factory.build trong thông số kỹ thuật của mình hay không, thay vì thực sự tạo đối tượng. (Sẽ là đơn giản nhất nếu bạn có thể).


2

Về câu trả lời được đăng ở trên, https://stackoverflow.com/a/35562805/2001785 , bạn không cần phải thêm mã vào nhà máy. Tôi thấy việc quá tải các phương thức trong thông số kỹ thuật trở nên dễ dàng hơn. Ví dụ: thay vì (kết hợp với mã nhà máy trong bài đăng được trích dẫn)

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

Tôi thích sử dụng (không có mã nhà máy được trích dẫn)

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

Bằng cách này, bạn không cần phải xem cả nhà máy và tệp thử nghiệm để hiểu hành vi của thử nghiệm.


1

Tôi thấy giải pháp sau là một cách rõ ràng hơn vì lệnh gọi lại được chạy / đặt ở cấp độ lớp.

# 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

Đây là đoạn mã tôi đã tạo để xử lý vấn đề này theo cách chung chung.
Nó sẽ bỏ qua mọi lệnh gọi lại được định cấu hình, bao gồm các lệnh gọi lại liên quan đến đường ray before_save_collection_association, nhưng nó sẽ không bỏ qua một số thao tác cần thiết để ActiveRecord hoạt động tốt, chẳng hạn như lệnh autosave_associated_records_for_gọi lại được tạo tự động.

# 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

sau đó sau:

create(:user, :skip_all_callbacks)

Không cần phải nói, YMMV, vì vậy hãy xem trong nhật ký kiểm tra những gì bạn thực sự bỏ qua. Có thể bạn có một viên ngọc thêm một cuộc gọi lại mà bạn thực sự cần và nó sẽ khiến các bài kiểm tra của bạn thất bại thảm hại hoặc từ mô hình chất béo 100 lần gọi lại của bạn, bạn chỉ cần một vài lần cho một bài kiểm tra cụ thể. Đối với những trường hợp đó, hãy thử:force_callbacks

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

TẶNG KEM

Đôi khi bạn cũng cần bỏ qua xác thực (tất cả nhằm nỗ lực làm cho kiểm tra nhanh hơn), sau đó thử với:

  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

Bạn chỉ có thể đặt lệnh gọi lại với một đặc điểm cho những trường hợp đó khi bạn muốn chạy nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.