Pular chamadas de retorno no Factory Girl e Rspec

103

Estou testando um modelo com um retorno de chamada after create que gostaria de executar apenas em algumas ocasiões durante o teste. Como posso pular / executar callbacks de uma 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
fonte

Respostas:

111

Não tenho certeza se é a melhor solução, mas consegui isso com sucesso 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

Executando sem retorno de chamada:

FactoryGirl.create(:user)

Executando com retorno de chamada:

FactoryGirl.create(:user_with_run_something)
luizbranco
fonte
3
Se você quiser pular uma :on => :createvalidação, useafter(:build) { |user| user.class.skip_callback(:validate, :create, :after, :run_something) }
James Chevalier
7
não seria melhor inverter a lógica de retorno de chamada de salto? Quer dizer, o padrão deve ser que, quando eu crio um objeto, os retornos de chamada sejam acionados, e devo usar um parâmetro diferente para o caso excepcional. então FactoryGirl.create (: user) deve criar o usuário acionando os callbacks, e FactoryGirl.create (: user_without_callbacks) deve criar o usuário sem os callbacks. Eu sei que isso é apenas uma modificação de "design", mas acho que isso pode evitar a quebra do código pré-existente e ser mais consistente.
Gnagno de
3
Como observa a solução de @Minimal, a Class.skip_callbackchamada será persistente em outros testes, portanto, se seus outros testes esperam que o retorno de chamada ocorra, eles falharão se você tentar inverter a lógica de retorno de chamada ignorada.
mpdaugherty
Acabei usando a resposta de @uberllama sobre stubbing com Mocha no after(:build)bloco. Isso permite que seu padrão de fábrica execute o retorno de chamada e não requer a redefinição do retorno de chamada após cada uso.
mpdaugherty
Você tem alguma ideia de como isso funcionará de outra maneira? stackoverflow.com/questions/35950470/…
Chris Hough
89

Quando você não quiser executar um retorno de chamada, faça o seguinte:

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

Esteja ciente de que skip_callback será persistente em outras especificações após ser executado, portanto, considere algo como o seguinte:

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

after do
  User.set_callback(:create, :after, :run_something)
end
Minimul
fonte
12
Eu gosto mais desta resposta porque ela afirma explicitamente que ignorar retornos de chamada ocorre no nível da classe e, portanto, continuaria a ignorar retornos de chamada em testes subsequentes.
siannopollo de
Eu gosto mais disso também. Não quero que minha fábrica permanentemente se comporte de maneira diferente. Quero pular para um determinado conjunto de testes.
theUtherSide
39

Nenhuma dessas soluções é boa. Eles desfiguram a classe removendo funcionalidade que deve ser removida da instância, não da classe.

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

Em vez de suprimir o retorno de chamada, estou suprimindo a funcionalidade do retorno de chamada. De certa forma, gosto mais dessa abordagem porque é mais explícita.

B Seven
fonte
1
Eu realmente gostei dessa resposta e me pergunto se algo assim, com alias para que a intenção seja imediatamente clara, deveria fazer parte do próprio FactoryGirl.
Giuseppe
Eu também gosto tanto dessa resposta que votaria contra todo o resto, mas parece que precisamos passar um bloco para o método definido, se seu retorno de chamada for do parente de around_*(por exemplo user.define_singleton_method(:around_callback_method){|&b| b.call }).
Quv
1
Não apenas uma solução melhor, mas por algum motivo o outro método não funcionou para mim. Quando eu o implementei, ele disse que não existia nenhum método de retorno de chamada, mas quando eu o deixei de fora, ele me pedia para fazer o stub das solicitações desnecessárias. Embora isso me leve a uma solução, alguém sabe por quê?
Babbz77 de
27

Eu gostaria de melhorar a resposta de @luizbranco para tornar o callback after_save mais reutilizável ao criar outros usuários.

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

Executando sem callback after_save:

FactoryGirl.create(:user)

Executando com callback after_save:

FactoryGirl.create(:user, :with_after_save_callback)

Em meu teste, prefiro criar usuários sem o retorno de chamada por padrão, porque os métodos usados ​​executam coisas extras que normalmente não quero em meus exemplos de teste.

---------- ATUALIZAÇÃO ------------ Eu parei de usar skip_callback porque havia alguns problemas de inconsistência no conjunto de testes.

Solução alternativa 1 (uso de 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

Solução alternativa 2 (minha abordagem preferida):

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
fonte
Você tem alguma ideia de como isso funcionará de outra maneira? stackoverflow.com/questions/35950470/…
Chris Hough
RuboCop se queixa de "Style / SingleLineMethods: Evite definições de método de linha única" para a Solução Alternativa 2, então vou precisar alterar a formatação, mas caso contrário, é perfeito!
coberlin
14

Rails 5 - skip_callbackaumentando o erro de argumento ao pular de uma fábrica de FactoryBot.

ArgumentError: After commit callback :whatever_callback has not been defined

Houve uma mudança no Rails 5 com a forma como skip_callback lida com callbacks não reconhecidos:

ActiveSupport :: Callbacks # skip_callback agora gera um ArgumentError se um retorno de chamada não reconhecido for removido

Quando skip_callbacké chamado da fábrica, o retorno de chamada real no modelo AR ainda não está definido.

Se você já tentou de tudo e puxou o cabelo como eu, aqui está sua solução (consegui pesquisando problemas do FactoryBot) ( NOTE a raise: falseparte ):

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

Sinta-se à vontade para usá-lo com quaisquer outras estratégias de sua preferência.

RudyOnRails
fonte
1
Ótimo, foi exatamente isso que aconteceu comigo. Observe que, se você removeu um retorno de chamada uma vez e tenta novamente, isso acontece, então é bem provável que isso seja acionado várias vezes para uma fábrica.
Slhck
6

Esta solução funciona para mim e você não precisa adicionar um bloco adicional à sua definição de fábrica:

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

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

Um esboço simples funcionou melhor para mim no Rspec 3

allow(User).to receive_messages(:run_something => nil)
samg
fonte
4
Você precisa configurá-lo para instâncias de User; :run_somethingnão é um método de 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 que você deve especificar ambos. Se usar antes e executar várias especificações, ele tentará desabilitar o retorno de chamada várias vezes. Terá sucesso na primeira vez, mas na segunda, o retorno de chamada não será mais definido. Então vai dar um erro

AndreiMotinga
fonte
Isso causou algumas falhas ofuscadas em um pacote em um projeto recente - recebi algo semelhante à resposta de @Sairam, mas o retorno de chamada não foi definido na classe entre os testes. Opa.
kfrz de
4

Ligar para skip_callback da minha fábrica foi problemático para mim.

No meu caso, tenho uma classe de documento com alguns retornos de chamada relacionados ao s3 antes e depois da criação que só quero executar quando for necessário testar a pilha completa. Caso contrário, quero pular esses retornos de chamada s3.

Quando tentei skip_callbacks em minha fábrica, persistiu esse salto de retorno de chamada mesmo quando eu criei um objeto de documento diretamente, sem usar uma fábrica. Em vez disso, usei stubs de mocha na chamada de pós-compilação e tudo está funcionando perfeitamente:

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
fonte
De todas as soluções aqui, e por ter a lógica dentro da fábrica, esta é a única que funciona com um before_validationgancho (tentando fazer skip_callbackcom qualquer um dos FactoryGirls beforeou afteropções para builde createnão funcionou)
Mike T
3

Isso funcionará com a sintaxe rspec atual (a partir desta postagem) e é muito mais limpo:

before do
   User.any_instance.stub :run_something
end
Zyren
fonte
isso está obsoleto no Rspec 3. Usar um stub regular funcionou para mim, veja minha resposta abaixo.
samg de
3

A resposta de James Chevalier sobre como pular o retorno de chamada before_validation não me ajudou, então se você se esforçar da mesma forma que eu aqui está a solução de trabalho:

no modelo:

before_validation :run_something, on: :create

na fábrica:

after(:build) { |obj| obj.class.skip_callback(:validation, :before, :run_something) }
Tetiana Chupryna
fonte
2
Acho que é preferível evitar isso. Ele pula os retornos de chamada para cada instância da classe (não apenas aqueles gerados pela garota da fábrica). Isso levará a alguns problemas de execução de especificações (ou seja, se a desativação ocorrer após a criação da fábrica inicial) que podem ser difíceis de depurar. Se este for o comportamento desejado na especificação / suporte, deve ser feito explicitamente: Model.skip_callback(...)
Kevin Sylvestre
2

No meu caso, o retorno de chamada está carregando algo no cache do redis. Mas eu não tinha / queria uma instância do Redis em execução no meu ambiente de teste.

after_create :load_to_cache

def load_to_cache
  Redis.load_to_cache
end

Para minha situação, semelhante à acima, acabei de load_to_cachecriar um esboço do meu método em meu spec_helper, com:

Redis.stub(:load_to_cache)

Além disso, em certas situações em que quero testar isso, só preciso descompactá-los no bloco anterior dos casos de teste Rspec correspondentes.

Eu sei que você pode ter algo mais complicado acontecendo no seu after_createou pode não achar isso muito elegante. Você pode tentar cancelar o callback definido em seu modelo, definindo um after_creategancho em seu Factory (consulte a documentação da factory_girl), onde você provavelmente pode definir o mesmo callback e retorno false, de acordo com a seção 'Cancelando callbacks' deste artigo . (Não tenho certeza sobre a ordem em que o retorno de chamada é executado, por isso não escolhi essa opção).

Por último, (desculpe, não consegui encontrar o artigo) Ruby permite que você use alguma meta-programação suja para liberar um gancho de retorno de chamada (você terá que reiniciá-lo). Eu acho que essa seria a opção menos preferida.

Bem, há mais uma coisa, não é realmente uma solução, mas veja se você consegue se safar com Factory.build em suas especificações, ao invés de realmente criar o objeto. (Seria o mais simples se você pudesse).

jake
fonte
2

Com relação à resposta postada acima, https://stackoverflow.com/a/35562805/2001785 , você não precisa adicionar o código de fábrica. Achei mais fácil sobrecarregar os métodos nas próprias especificações. Por exemplo, em vez de (em conjunto com o código de fábrica na postagem citada)

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

Eu gosto de usar (sem o 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

Dessa forma, você não precisa examinar os arquivos de fábrica e de teste para entender o comportamento do teste.

bhfailor
fonte
1

Descobri que a solução a seguir é uma maneira mais limpa, já que o retorno de chamada é executado / definido no nível da 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
Sairam
fonte
0

Aqui está um snippet que criei para lidar com isso de uma forma genérica.
Ele irá pular todos os callbacks configurados, incluindo callbacks relacionados ao rails before_save_collection_association, mas não vai pular alguns necessários para fazer o ActiveRecord funcionar bem, como autosave_associated_records_for_callbacks gerados 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

então mais tarde:

create(:user, :skip_all_callbacks)

Desnecessário dizer, YMMV, então dê uma olhada nos logs de teste o que você realmente está pulando. Talvez você tenha uma joia adicionando um retorno de chamada que você realmente precisa e isso fará com que seus testes falhem miseravelmente ou de seu modelo de gordura de 100 retornos de chamada, você só precisa de alguns para um teste específico. Para esses casos, tente o temporário:force_callbacks

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

BÔNUS

Às vezes, você também precisa pular as validações (tudo em um esforço para tornar os testes mais rápidos) e, em seguida, tente:

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

Você pode apenas definir o retorno de chamada com uma característica para aquelas instâncias quando quiser executá-lo.

user6520080
fonte