Omitir devoluciones de llamada en Factory Girl y Rspec

103

Estoy probando un modelo con una devolución de llamada posterior a la creación que me gustaría ejecutar solo en algunas ocasiones durante la prueba. ¿Cómo puedo omitir / ejecutar devoluciones de llamada desde una fábrica?

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

Fábrica:

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

    factory :with_run_something do
      # run callback
  end
end
luizbranco
fuente

Respuestas:

111

No estoy seguro de si es la mejor solución, pero lo he logrado con éxito usando:

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

Ejecutando sin devolución de llamada:

FactoryGirl.create(:user)

Ejecutando con devolución de llamada:

FactoryGirl.create(:user_with_run_something)
luizbranco
fuente
3
Si desea omitir una :on => :createvalidación, useafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier
7
¿No sería mejor invertir la lógica de devolución de llamada saltada? Quiero decir, el valor predeterminado debería ser que cuando creo un objeto se activen las devoluciones de llamada, y debería usar un parámetro diferente para el caso excepcional. por lo que FactoryGirl.create (: user) debe crear el usuario que activa las devoluciones de llamada, y FactoryGirl.create (: user_without_callbacks) debe crear el usuario sin las devoluciones de llamada. Sé que esto es solo una modificación de "diseño", pero creo que esto puede evitar romper el código preexistente y ser más consistente.
Gnagno
3
Como señala la solución de @ Minimal, la Class.skip_callbackllamada será persistente en otras pruebas, por lo que si sus otras pruebas esperan que se produzca la devolución de llamada, fallarán si intenta invertir la lógica de devolución de llamada de omisión.
mpdaugherty
Terminé usando la respuesta de @ uberllama sobre tropezar con Mocha en el after(:build)bloque. Esto permite que su configuración predeterminada de fábrica ejecute la devolución de llamada y no requiere restablecer la devolución de llamada después de cada uso.
mpdaugherty
¿Tiene alguna idea de que esto funcione al revés? stackoverflow.com/questions/35950470/…
Chris Hough
89

Cuando no desee ejecutar una devolución de llamada, haga lo siguiente:

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

Tenga en cuenta que skip_callback será persistente en otras especificaciones después de que se ejecute, por lo tanto, considere algo como lo siguiente:

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

after do
  User.set_callback(:create, :after, :run_something)
end
Mínimo
fuente
12
Me gusta más esta respuesta porque establece explícitamente que omitir las devoluciones de llamada se mantiene en el nivel de clase y, por lo tanto, continuaría omitiendo las devoluciones de llamada en las pruebas posteriores.
siannopollo
También me gusta más esto. No quiero que mi fábrica se comporte permanentemente de manera diferente. Quiero omitirlo para un conjunto particular de pruebas.
theUtherSide
39

Ninguna de estas soluciones es buena. Ellos desfiguran la clase eliminando la funcionalidad que debería eliminarse de la instancia, no de la clase.

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

En lugar de suprimir la devolución de llamada, estoy suprimiendo la funcionalidad de la devolución de llamada. En cierto modo, me gusta más este enfoque porque es más explícito.

B siete
fuente
1
Realmente me gusta esta respuesta, y me pregunto si algo como esto, con un alias para que la intención sea clara de inmediato, debería ser parte de FactoryGirl.
Giuseppe
También me gusta tanto esta respuesta que rechazaría todo lo demás, pero parece que necesitamos pasar un bloque al método definido, si tu devolución de llamada es pariente de around_*(por ejemplo user.define_singleton_method(:around_callback_method){|&b| b.call }).
Quv
1
No solo una mejor solución, sino que, por alguna razón, el otro método no funcionó para mí. Cuando lo implementé, decía que no existía ningún método de devolución de llamada, pero cuando lo dejé, me pedía que eliminara las solicitudes innecesarias. Aunque me lleve a una solución, ¿alguien sabe por qué podría ser?
Babbz77
27

Me gustaría mejorar la respuesta de @luizbranco para que la devolución de llamada after_save sea más reutilizable al crear otros usuarios.

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

Ejecutando sin devolución de llamada after_save:

FactoryGirl.create(:user)

Ejecutando con devolución de llamada after_save:

FactoryGirl.create(:user, :with_after_save_callback)

En mi prueba, prefiero crear usuarios sin la devolución de llamada de forma predeterminada porque los métodos utilizados ejecutan cosas adicionales que normalmente no quiero en mis ejemplos de prueba.

---------- ACTUALIZACIÓN ------------ Dejé de usar skip_callback porque había algunos problemas de inconsistencia en el conjunto de pruebas.

Solución alternativa 1 (uso de stub y 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

Solución alternativa 2 (mi enfoque preferido):

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
fuente
¿Tiene alguna idea de que esto funcione al revés? stackoverflow.com/questions/35950470/…
Chris Hough
RuboCop se queja con "Style / SingleLineMethods: Evite las definiciones de métodos de una sola línea" para la Solución alternativa 2, así que tendré que cambiar el formato, ¡pero por lo demás es perfecto!
coberlin
14

Rails 5: skip_callbackerror de argumento al saltar de una fábrica FactoryBot.

ArgumentError: After commit callback :whatever_callback has not been defined

Hubo un cambio en Rails 5 en la forma en que skip_callback maneja las devoluciones de llamada no reconocidas:

ActiveSupport :: Callbacks # skip_callback ahora genera un ArgumentError si se elimina una devolución de llamada no reconocida

Cuando skip_callbackse llama desde la fábrica, la devolución de llamada real en el modelo AR aún no está definida.

Si ha intentado todo y se ha arrancado el pelo como yo, aquí está su solución (la obtuve al buscar problemas de FactoryBot) ( NOTA la raise: falseparte ):

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

Siéntase libre de usarlo con cualquier otra estrategia que prefiera.

RudyOnRails
fuente
1
Genial, esto es exactamente lo que me pasó. Tenga en cuenta que si ha eliminado una devolución de llamada una vez y lo vuelve a intentar, esto sucede, por lo que es muy probable que se active varias veces para una fábrica.
Slhck el
6

Esta solución funciona para mí y no tienes que agregar un bloque adicional a tu definición de fábrica:

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

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

Un simple stub funcionó mejor para mí en Rspec 3

allow(User).to receive_messages(:run_something => nil)
samg
fuente
4
Necesitaría configurarlo para instancias de User; :run_somethingno es un método de clase.
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 : debe especificar ambos. Si solo se usa antes y ejecuta varias especificaciones, intentará deshabilitar la devolución de llamada varias veces. Tendrá éxito la primera vez, pero en la segunda, la devolución de llamada ya no se definirá. Entonces saldrá un error

AndreiMotinga
fuente
Esto causó algunas fallas ofuscadas en una suite en un proyecto reciente: tenía algo similar a la respuesta de @ Sairam, pero la devolución de llamada se dejaba sin configurar en la clase entre pruebas. ¡Ups!
kfrz
4

Llamar a skip_callback desde mi fábrica resultó problemático para mí.

En mi caso, tengo una clase de documento con algunas devoluciones de llamada relacionadas con s3 antes y después de la creación que solo quiero ejecutar cuando es necesario probar la pila completa. De lo contrario, quiero omitir esas devoluciones de llamada s3.

Cuando probé skip_callbacks en mi fábrica, persistió ese salto de devolución de llamada incluso cuando creé un objeto de documento directamente, sin usar una fábrica. Entonces, en cambio, usé talones de moca en la llamada de compilación posterior y todo funciona perfectamente:

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
fuente
De todas las soluciones aquí, y por tener la lógica dentro de la fábrica, esta es la única que funciona con un before_validationgancho (tratando de hacerlo skip_callbackcon cualquiera de las opciones de FactoryGirl beforeo afterpara buildy createno funcionó)
Mike T
3

Esto funcionará con la sintaxis actual de rspec (a partir de esta publicación) y es mucho más limpio:

before do
   User.any_instance.stub :run_something
end
Zyren
fuente
esto está en desuso en Rspec 3. El uso de un código auxiliar regular funcionó para mí, vea mi respuesta a continuación.
samg
3

La respuesta de James Chevalier sobre cómo omitir la devolución de llamada before_validation no me ayudó, por lo que si rezagas lo mismo que yo, aquí hay una solución que funciona:

en modelo:

before_validation :run_something, on: :create

en fábrica:

after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
Tetiana Chupryna
fuente
2
Creo que es preferible evitarlo. Omite las devoluciones de llamada para cada instancia de la clase (no solo las generadas por Factory Girl). Esto conducirá a algunos problemas de ejecución de especificaciones (es decir, si la desactivación ocurre después de la construcción de la fábrica inicial) que pueden ser difíciles de depurar. Si este es el comportamiento deseado en la especificación / soporte, debe hacerse explícitamente: Model.skip_callback(...)
Kevin Sylvestre
2

En mi caso, tengo la devolución de llamada cargando algo en mi caché de redis. Pero luego no tenía / quería una instancia de redis ejecutándose para mi entorno de prueba.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

Para mi situación, similar a la anterior, acabo de copiar mi load_to_cachemétodo en mi spec_helper, con:

Redis.stub(:load_to_cache)

Además, en cierta situación en la que quiero probar esto, solo tengo que deshacerlos en el bloque anterior de los casos de prueba Rspec correspondientes.

Sé que es posible que tenga algo más complicado en su after_createinterior o que no lo encuentre muy elegante. Puede intentar cancelar la devolución de llamada definida en su modelo, definiendo un after_creategancho en su Factory (consulte los documentos de factory_girl), donde probablemente pueda definir la misma devolución de llamada y devolución false, de acuerdo con la sección 'Cancelación de devoluciones de llamada' de este artículo . (No estoy seguro del orden en el que se ejecutan las devoluciones de llamada, por lo que no elegí esta opción).

Por último, (lo siento, no puedo encontrar el artículo) Ruby te permite usar una metaprogramación sucia para desenganchar un gancho de devolución de llamada (tendrás que restablecerlo). Supongo que esta sería la opción menos preferida.

Bueno, hay una cosa más, no realmente una solución, pero vea si puede salirse con la suya con Factory.build en sus especificaciones, en lugar de crear el objeto. (Sería lo más simple si se pudiera).

jake
fuente
2

Con respecto a la respuesta publicada anteriormente, https://stackoverflow.com/a/35562805/2001785 , no es necesario que agregue el código a la fábrica. Me resultó más fácil sobrecargar los métodos en las propias especificaciones. Por ejemplo, en lugar de (junto con el código de fábrica en la publicación citada)

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

Me gusta usar (sin el código de fábrica citado)

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

De esta manera, no es necesario mirar tanto la fábrica como los archivos de prueba para comprender el comportamiento de la prueba.

bhfailor
fuente
1

Encontré que la siguiente solución es una forma más limpia, ya que la devolución de llamada se ejecuta / configura a nivel de clase.

# 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
fuente
0

Aquí hay un fragmento que creé para manejar esto de una manera genérica.
Omitirá todas las devoluciones de llamada configuradas, incluidas las devoluciones de llamada relacionadas con rieles before_save_collection_association, pero no omitirá algunas necesarias para que ActiveRecord funcione correctamente, como las autosave_associated_records_for_devoluciones de llamada generadas automáticamente.

# 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

Entonces despúes:

create(:user, :skip_all_callbacks)

No hace falta decirlo, YMMV, así que eche un vistazo a los registros de prueba qué es lo que realmente se está saltando. Tal vez tenga una gema que agregue una devolución de llamada que realmente necesita y hará que sus pruebas fallen miserablemente o de su modelo de grasa de 100 devoluciones de llamada, solo necesita un par para una prueba específica. Para esos casos, pruebe el transitorio:force_callbacks

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

PRIMA

A veces también necesita omitir las validaciones (todo en un esfuerzo por hacer las pruebas más rápidas), luego intente con:

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

Puede configurar la devolución de llamada con un rasgo para esas instancias cuando desee ejecutarlo.

usuario6520080
fuente