RSpec: Espere mudar vários

86

Quero verificar muitas mudanças em um modelo ao enviar um formulário em uma especificação de recurso. Por exemplo, quero ter certeza de que o nome de usuário foi alterado de X para Y e que a senha criptografada foi alterada por qualquer valor.

Sei que já existem algumas perguntas sobre isso, mas não encontrei uma resposta adequada para mim. A resposta mais precisa parece ser aquela ChangeMultiplede Michael Johnston aqui: É possível para RSpec esperar mudanças em duas tabelas? . Sua desvantagem é que só se verifica mudanças explícitas de valores conhecidos para valores conhecidos.

Criei um pseudocódigo sobre como acho que um combinador melhor poderia ser:

expect {
  click_button 'Save'
}.to change_multiple { @user.reload }.with_expectations(
  name:               {from: 'donald', to: 'gustav'},
  updated_at:         {by: 4},
  great_field:        {by_at_leaset: 23},
  encrypted_password: true,  # Must change
  created_at:         false, # Must not change
  some_other_field:   nil    # Doesn't matter, but want to denote here that this field exists
)

Também criei o esqueleto básico do ChangeMultiplecombinador como este:

module RSpec
  module Matchers
    def change_multiple(receiver=nil, message=nil, &block)
      BuiltIn::ChangeMultiple.new(receiver, message, &block)
    end

    module BuiltIn
      class ChangeMultiple < Change
        def with_expectations(expectations)
          # What to do here? How do I add the expectations passed as argument?
        end
      end
    end
  end
end

Mas agora já estou recebendo este erro:

 Failure/Error: expect {
   You must pass an argument rather than a block to use the provided matcher (nil), or the matcher must implement `supports_block_expectations?`.
 # ./spec/features/user/registration/edit_spec.rb:20:in `block (2 levels) in <top (required)>'
 # /Users/josh/.rvm/gems/ruby-2.1.0@base/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:268:in `load'
 # /Users/josh/.rvm/gems/ruby-2.1.0@base/gems/activesupport-4.2.0/lib/active_support/dependencies.rb:268:in `block in load'

Qualquer ajuda na criação deste matcher personalizado é muito apreciada.

Joshua Muheim
fonte

Respostas:

183

No RSpec 3, você pode configurar várias condições de uma vez (para que a regra de expectativa única não seja quebrada). Seria exatamente como:

expect {
  click_button 'Save'
  @user.reload
}.to change { @user.name }.from('donald').to('gustav')
 .and change { @user.updated_at }.by(4)
 .and change { @user.great_field }.by_at_least(23}
 .and change { @user.encrypted_password }

Porém, não é uma solução completa - até onde minha pesquisa foi, ainda não há uma maneira fácil de fazer and_not. Também não tenho certeza sobre sua última verificação (se não importa, por que testá-lo?). Naturalmente, você deve ser capaz de envolvê-lo em seu combinador personalizado .

BroiSatse
fonte
6
se você quiser que várias coisas não sejam alteradas, basta usar.and change { @something }.by(0)
stevenspiel
1
Você pode adicionar um segundo exemplo com todos os parênteses? É difícil entender quais métodos são encadeados
Cyril Duchon-Doris
Minha resposta funciona para Ruby 2 e parece funcionar .should_notpara qualquer um que precise
Zack Morris
37

Se você quiser testar se vários registros não foram alterados, você pode inverter um matcher usando RSpec::Matchers.define_negated_matcher. Então, adicione

RSpec::Matchers.define_negated_matcher :not_change, :change

no topo do seu arquivo (ou no seu rails_helper.rb) e então você pode encadear usando and:

expect{described_class.reorder}.to not_change{ruleset.reload.position}.
    and not_change{simple_ruleset.reload.position}
Matthew Hinea
fonte
2

A resposta aceita não é 100% correta, pois o suporte completo de correspondência de compostos para change {}foi adicionado na versão 3.1.0 do RSpec . Se você tentar executar o código fornecido na resposta aceita com o RSpec versão 3.0, receberá um erro.

Para usar matchers compostos com change {}, existem duas maneiras;

  • O primeiro é, você deve ter pelo menos RSpec versão 3.1.0 .
  • O segundo é que você tem que adicionar algo def supports_block_expectations?; true; endà RSpec::Matchers::BuiltIn::Compoundclasse, corrigindo-a ou editando diretamente a cópia local da gema. Uma nota importante: desta forma não é completamente equivalente à primeira, o expect {}bloco é executado várias vezes desta forma!

A solicitação pull que adicionou o suporte total à funcionalidade de matchers compostos pode ser encontrada aqui .

Zoo Foo Bar
fonte
2

A resposta de BroiSatse é a melhor, mas se você estiver usando RSpec 2 (ou tiver correspondências mais complexas como .should_not), este método também funciona:

lambda {
  lambda {
    lambda {
      lambda {
        click_button 'Save'
        @user.reload
      }.should change {@user.name}.from('donald').to('gustav')
    }.should change {@user.updated_at}.by(4)
  }.should change {@user.great_field}.by_at_least(23)
}.should change {@user.encrypted_password}
Zack Morris
fonte
1
Ah, boa ideia! Você provavelmente poderia construir um invólucro para torná-lo um pouco mais fácil de ler!
BroiSatse