Salta le richiamate su Factory Girl e Rspec


103

Sto testando un modello con un callback dopo la creazione che vorrei eseguire solo in alcune occasioni durante il test. Come posso saltare / eseguire i callback da una fabbrica?

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

Fabbrica:

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

    factory :with_run_something do
      # run callback
  end
end

Risposte:


111

Non sono sicuro che sia la soluzione migliore, ma l'ho ottenuta con successo utilizzando:

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

In esecuzione senza richiamata:

FactoryGirl.create(:user)

In esecuzione con callback:

FactoryGirl.create(:user_with_run_something)

3
Se vuoi saltare una :on => :createconvalida, usaafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier

7
non sarebbe meglio invertire la logica di richiamata saltata? Voglio dire, l'impostazione predefinita dovrebbe essere che quando creo un oggetto i callback vengono attivati ​​e dovrei usare un parametro diverso per il caso eccezionale. quindi FactoryGirl.create (: user) dovrebbe creare l'utente che attiva i callback e FactoryGirl.create (: user_without_callbacks) dovrebbe creare l'utente senza i callback. So che questa è solo una modifica di "progettazione", ma penso che questo possa evitare di rompere il codice preesistente ed essere più coerente.
Gnagno

3
Come nota la soluzione di @ Minimal, la Class.skip_callbackchiamata sarà persistente in altri test, quindi se gli altri test si aspettano che si verifichi il callback, falliranno se si tenta di invertire la logica di callback saltata.
mpdaugherty

Ho finito per usare la risposta di @ uberllama sullo stubbing con Mocha nel after(:build)blocco. Ciò consente alle impostazioni di fabbrica di eseguire la richiamata e non richiede il ripristino della richiamata dopo ogni utilizzo.
mpdaugherty

Hai qualche idea che questo funzioni nell'altro modo? stackoverflow.com/questions/35950470/…
Chris Hough,

89

Quando non desideri eseguire una richiamata, procedi come segue:

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

Tieni presente che skip_callback sarà persistente per altre specifiche dopo che è stato eseguito, quindi considera qualcosa di simile a quanto segue:

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

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

12
Questa risposta mi piace di più perché afferma esplicitamente che saltare i callback si blocca a livello di classe e quindi continuerebbe a saltare i callback nei test successivi.
siannopollo

Anche questo mi piace di più. Non voglio che la mia fabbrica si comporti permanentemente in modo diverso. Voglio saltarlo per una particolare serie di test.
theUtherSide

39

Nessuna di queste soluzioni è buona. Deturpano la classe rimuovendo le funzionalità che dovrebbero essere rimosse dall'istanza, non dalla classe.

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

Invece di sopprimere la richiamata, sto sopprimendo la funzionalità della richiamata. In un certo senso, questo approccio mi piace di più perché è più esplicito.


1
Mi piace molto questa risposta e mi chiedo se qualcosa del genere, alias in modo che l'intento sia immediatamente chiaro, dovrebbe far parte di FactoryGirl stessa.
Giuseppe

Mi piace anche questa risposta così tanto che avrei downvote tutto il resto, ma sembra che dobbiamo passare un blocco al metodo definito, se è il tuo callback è il parente di around_*(ad esempio user.define_singleton_method(:around_callback_method){|&b| b.call }).
Quv

1
Non solo una soluzione migliore, ma per qualche motivo l'altro metodo non ha funzionato per me. Quando l'ho implementato ha detto che non esisteva alcun metodo di callback, ma quando l'ho omesso mi chiedeva di bloccare le richieste non necessarie. Anche se mi porta a una soluzione, qualcuno sa perché potrebbe essere?
Babbz77

27

Vorrei apportare un miglioramento alla risposta di @luizbranco per rendere il callback after_save più riutilizzabile durante la creazione di altri utenti.

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

In esecuzione senza callback after_save:

FactoryGirl.create(:user)

Esecuzione con callback after_save:

FactoryGirl.create(:user, :with_after_save_callback)

Nel mio test, preferisco creare utenti senza il callback per impostazione predefinita perché i metodi utilizzati eseguono cose extra che normalmente non voglio nei miei esempi di test.

---------- AGGIORNAMENTO ------------ Ho smesso di usare skip_callback perché c'erano alcuni problemi di incoerenza nella suite di test.

Soluzione alternativa 1 (uso di stub e 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

Soluzione alternativa 2 (il mio approccio preferito):

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

Hai qualche idea che questo funzioni nell'altro modo? stackoverflow.com/questions/35950470/…
Chris Hough,

RuboCop si lamenta con "Style / SingleLineMethods: Evita definizioni di metodi a riga singola" per la soluzione alternativa 2, quindi dovrò cambiare la formattazione, ma per il resto è perfetto!
coberlin

14

Rails 5 - skip_callbacksollevamento di un errore di argomento quando si salta da una factory FactoryBot.

ArgumentError: After commit callback :whatever_callback has not been defined

C'è stato un cambiamento in Rails 5 con il modo in cui skip_callback gestisce i callback non riconosciuti:

ActiveSupport :: Callbacks # skip_callback ora solleva un ArgumentError se viene rimosso un callback non riconosciuto

Quando skip_callbackviene richiamato dalla fabbrica, il callback reale nel modello AR non è ancora definito.

Se hai provato di tutto e ti sei tirato i capelli come me, ecco la tua soluzione (ottenuta dalla ricerca di problemi FactoryBot) ( NOTA la raise: falseparte ):

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

Sentiti libero di usarlo con qualunque altra strategia preferisci.


1
Fantastico, questo è esattamente quello che è successo a me. Tieni presente che se hai rimosso una richiamata una volta e riprova, ciò accade, quindi è molto probabile che venga attivato più volte per una fabbrica.
slhck

6

Questa soluzione funziona per me e non devi aggiungere un blocco aggiuntivo alla tua definizione di fabbrica:

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

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

5

Un semplice stub ha funzionato meglio per me in Rspec 3

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

4
Dovresti configurarlo per istanze di User; :run_somethingnon è un metodo di classe.
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

Nota importante che dovresti specificare entrambi. Se usi solo prima ed esegui più specifiche, proverà a disabilitare la richiamata più volte. Avrà successo la prima volta, ma la seconda il callback non verrà più definito. Quindi verrà visualizzato un errore


Ciò ha causato alcuni errori offuscati in una suite su un progetto recente: avevo qualcosa di simile alla risposta di @ Sairam ma il callback non era stato impostato nella classe tra i test. Ops.
kfrz

4

Chiamare skip_callback dalla mia fabbrica si è rivelato problematico per me.

Nel mio caso, ho una classe di documenti con alcuni callback relativi a s3 prima e dopo la creazione che voglio eseguire solo quando è necessario testare lo stack completo. Altrimenti, voglio saltare quei callback s3.

Quando ho provato skip_callbacks nella mia factory, è persistito che il callback salta anche quando ho creato un oggetto documento direttamente, senza utilizzare una factory. Quindi, invece, ho usato mocha stub nella chiamata dopo la build e tutto funziona perfettamente:

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

Di tutte le soluzioni qui, e per avere la logica all'interno della fabbrica, questa è l'unica che funziona con un before_validationgancio (cercando di fare skip_callbackcon una qualsiasi delle FactoryGirl beforeo delle afteropzioni builde createnon ha funzionato)
Mike T

3

Funzionerà con la sintassi corrente di rspec (come in questo post) ed è molto più pulito:

before do
   User.any_instance.stub :run_something
end

questo è deprecato in Rspec 3. L'utilizzo di uno stub regolare ha funzionato per me, vedere la mia risposta di seguito.
samg

3

La risposta di James Chevalier su come saltare la richiamata before_validation non mi ha aiutato, quindi se ti allontani come me ecco la soluzione funzionante:

nel modello:

before_validation :run_something, on: :create

in fabbrica:

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

2
Penso che sia preferibile evitare questo. Salta i callback per ogni istanza della classe (non solo quelli generati da factory girl). Questo porterà ad alcuni problemi di esecuzione delle specifiche (cioè se la disabilitazione avviene dopo la costruzione della factory iniziale) che possono essere difficili da eseguire il debug. Se questo è il comportamento desiderato nelle specifiche / supporto, dovrebbe essere fatto esplicitamente: Model.skip_callback(...)
Kevin Sylvestre

2

Nel mio caso ho la richiamata che carica qualcosa nella mia cache di Redis. Ma poi non avevo / desideravo un'istanza redis in esecuzione per il mio ambiente di test.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

Per la mia situazione, simile a quella sopra, ho appena bloccato il mio load_to_cachemetodo nel mio spec_helper, con:

Redis.stub(:load_to_cache)

Inoltre, in alcune situazioni in cui voglio testarlo, devo solo rimuovere lo stub nel blocco precedente dei corrispondenti casi di test Rspec.

So che potresti avere qualcosa di più complicato che sta accadendo nel tuo after_createo potresti non trovarlo molto elegante. Puoi provare ad annullare la richiamata definita nel tuo modello, definendo un after_createhook nella tua Factory (fare riferimento alla documentazione di factory_girl), dove probabilmente puoi definire la stessa richiamata e ritorno false, secondo la sezione "Annullamento delle richiamate" di questo articolo . (Non sono sicuro dell'ordine in cui vengono eseguiti i callback, motivo per cui non ho scelto questa opzione).

Infine, (scusate non sono riuscito a trovare l'articolo) Ruby vi permette di usare un po 'di sporca meta programmazione per sganciare un callback hook (dovrete resettarlo). Immagino che questa sarebbe l'opzione meno preferita.

Bene, c'è un'altra cosa, non proprio una soluzione, ma vedi se riesci a farla franca con Factory.build nelle tue specifiche, invece di creare effettivamente l'oggetto. (Sarebbe il più semplice se puoi).


2

Per quanto riguarda la risposta pubblicata sopra, https://stackoverflow.com/a/35562805/2001785 , non è necessario aggiungere il codice alla fabbrica. Ho trovato più facile sovraccaricare i metodi nelle specifiche stesse. Ad esempio, invece di (in combinazione con il codice di fabbrica nel post citato)

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

Mi piace usare (senza il codice di fabbrica citato)

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

In questo modo non è necessario guardare sia i file di fabbrica che i file di test per comprendere il comportamento del test.


1

Ho trovato la seguente soluzione per essere un modo più pulito poiché il callback viene eseguito / impostato a livello di classe.

# 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

Ecco uno snippet che ho creato per gestirlo in modo generico.
Salterà ogni callback configurato, inclusi i callback relativi a rails come before_save_collection_association, ma non salterà alcuni necessari per far funzionare correttamente ActiveRecord, come i autosave_associated_records_for_callback generati automaticamente .

# 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

poi più tardi:

create(:user, :skip_all_callbacks)

Inutile dire, YMMV, quindi dai un'occhiata ai registri di prova cosa stai veramente saltando. Forse hai una gemma che aggiunge un callback di cui hai davvero bisogno e farà fallire miseramente i tuoi test o dal tuo modello grasso di 100 callback ti servono solo un paio per un test specifico. In questi casi, prova il transitorio:force_callbacks

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

BONUS

A volte è necessario anche saltare le convalide (tutto nel tentativo di rendere i test più veloci), quindi provare con:

  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

Potresti semplicemente impostare la richiamata con una caratteristica per quelle istanze quando vuoi eseguirla.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.