Testando módulos no rspec

175

Quais são as melhores práticas para testar módulos no rspec? Eu tenho alguns módulos que são incluídos em alguns modelos e, por enquanto, eu simplesmente tenho testes duplicados para cada modelo (com poucas diferenças). Existe uma maneira de secar?

Andrius
fonte

Respostas:

219

O caminho rad =>

let(:dummy_class) { Class.new { include ModuleToBeTested } }

Como alternativa, você pode estender a classe de teste com seu módulo:

let(:dummy_class) { Class.new { extend ModuleToBeTested } }

Usar 'let' é melhor do que usar uma variável de instância para definir a classe dummy no antes (: each)

Quando usar o RSpec let ()?

metakungfu
fonte
1
Agradável. Isso me ajudou a evitar todos os tipos de problemas com os testes de abrangência dos ivars da classe. Deu os nomes das classes atribuindo constantes.
captainpete
3
@lulalala Não, é uma super classe: ruby-doc.org/core-2.0.0/Class.html#method-c-new Para testar os módulos, faça algo parecido com isto:let(:dummy_class) { Class.new { include ModuleToBeTested } }
Timo
26
Maneira rad. Normalmente, faço: let(:class_instance) { (Class.new { include Super::Duper::Module }).new }dessa maneira, obtenho a variável de instância mais usada para testar de qualquer maneira.
Automatico
3
usando includenão funciona para mim, mas extendfazlet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W
8
Mesmo radder:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne 17/17/17
108

O que Mike disse. Aqui está um exemplo trivial:

código do módulo ...

module Say
  def hello
    "hello"
  end
end

fragmento de especificação ...

class DummyClass
end

before(:each) do
  @dummy_class = DummyClass.new
  @dummy_class.extend(Say)
end

it "get hello string" do
  expect(@dummy_class.hello).to eq "hello"
end
Karmen Blake
fonte
3
Algum motivo para você não estar include Saydentro da declaração DummyClass em vez de ligar extend?
Grant Birchmeier
2
grant-birchmeier, ele está inserindo extenda instância da classe, ou seja, depois de newter sido chamado. Se você estava fazendo isso antes newé chamado então você está certo você usariainclude
Hedgehog
8
Editei o código para ser mais conciso. @dummy_class = Class.new {extend Say} é tudo que você precisa para testar um módulo. Suspeito que as pessoas prefiram isso, já que os desenvolvedores geralmente não gostam de digitar mais do que o necessário.
Tim Harper
@ TimHarper Tentei, mas os métodos de instância se tornaram métodos de classe. Pensamentos?
Lulalala
6
Por que você definiria a DummyClassconstante? Por que não apenas @dummy_class = Class.new? Agora você está poluindo seu ambiente de teste com uma definição de classe desnecessária. Este DummyClass é definido para todas e cada uma de suas especificações e, na próxima especificação, onde você decide usar a mesma abordagem e reabrir a definição de DummyClass, ela já pode conter algo (embora neste exemplo trivial a definição seja estritamente vazia, na vida real casos de uso é provável que algo é adicionado em algum momento e, em seguida, esta abordagem torna-se perigoso).
Timo
29

Para módulos que podem ser testados isoladamente ou zombando da classe, eu gosto de algo como:

módulo:

module MyModule
  def hallo
    "hallo"
  end
end

especificação:

describe MyModule do
  include MyModule

  it { hallo.should == "hallo" }
end

Pode parecer errado seqüestrar grupos de exemplos aninhados, mas eu gosto da concisão. Alguma ideia?

Frank C. Schuetz
fonte
1
Eu gosto disso, é tão direto.
iain
2
Pode atrapalhar o rspec. Eu acho que usar o letmétodo descrito por @metakungfu é melhor.
Automatico
@ Cort3z Você definitivamente precisa ter certeza de que os nomes dos métodos não colidem. Só estou usando essa abordagem quando as coisas são realmente simples.
Frank C. Schuetz
Isso atrapalhou minha suíte de testes devido à colisão de nomes.
Roxxypoxxy 17/09/19
24

Encontrei uma solução melhor na página inicial do rspec. Aparentemente, ele suporta grupos de exemplos compartilhados. Em https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples !

Grupos de exemplos compartilhados

Você pode criar grupos de exemplos compartilhados e incluir esses grupos em outros grupos.

Suponha que você tenha algum comportamento que se aplique a todas as edições do seu produto, grandes e pequenas.

Primeiro, considere o comportamento "compartilhado":

shared_examples_for "all editions" do   
  it "should behave like all editions" do   
  end 
end

quando você precisar definir o comportamento para as edições Grandes e Pequenas, faça referência ao comportamento compartilhado usando o método it_should_behave_like ().

describe "SmallEdition" do  
  it_should_behave_like "all editions"
  it "should also behave like a small edition" do   
  end 
end
Andrius
fonte
21

Em primeiro lugar, você poderia criar uma classe fictícia em seu script de teste e incluir o módulo nisso? Em seguida, teste se a classe dummy tem o comportamento da maneira que você esperaria.

EDIT: Se, como apontado nos comentários, o módulo espera que alguns comportamentos estejam presentes na classe em que ele é misturado, tentarei implementar manequins desses comportamentos. Apenas o suficiente para deixar o módulo feliz em desempenhar suas funções.

Dito isso, eu ficaria um pouco nervoso com meu design quando um módulo espera muito da classe host (dizemos "host"?) - Se eu ainda não herdar de uma classe base ou não puder injetar a nova funcionalidade na árvore de herança, acho que tentaria minimizar quaisquer expectativas que um módulo possa ter. Minha preocupação é que meu design comece a desenvolver algumas áreas de inflexibilidade desagradável.

Mike Woodhouse
fonte
E se o meu módulo depender da classe ter certos atributos e comportamento?
Andrius
10

A resposta aceita é a resposta certa, no entanto, eu queria adicionar um exemplo de como usar rpsecs shared_examples_fore it_behaves_likemétodos. Menciono alguns truques no trecho de código, mas para obter mais informações, consulte este relishapp-rspec-guide .

Com isso, você pode testar seu módulo em qualquer uma das classes que o incluem. Então você realmente está testando o que usa em seu aplicativo.

Vamos ver um exemplo:

# Lets assume a Movable module
module Movable
  def self.movable_class?
    true
  end

  def has_feets?
    true
  end
end

# Include Movable into Person and Animal
class Person < ActiveRecord::Base
  include Movable
end

class Animal < ActiveRecord::Base
  include Movable
end

Agora vamos criar especificações para o nosso módulo: movable_spec.rb

shared_examples_for Movable do
  context 'with an instance' do
    before(:each) do
      # described_class points on the class, if you need an instance of it: 
      @obj = described_class.new

      # or you can use a parameter see below Animal test
      @obj = obj if obj.present?
    end

    it 'should have feets' do
      @obj.has_feets?.should be_true
    end
  end

  context 'class methods' do
    it 'should be a movable class' do
      described_class.movable_class?.should be_true
    end
  end
end

# Now list every model in your app to test them properly

describe Person do
  it_behaves_like Movable
end

describe Animal do
  it_behaves_like Movable do
    let(:obj) { Animal.new({ :name => 'capybara' }) }
  end
end
p1100i
fonte
6

A respeito:

describe MyModule do
  subject { Object.new.extend(MyModule) }
  it "does stuff" do
    expect(subject.does_stuff?).to be_true
  end
end
Matt Connolly
fonte
6

Eu sugeriria que, para os módulos maiores e mais utilizados, você deveria optar pelos "Grupos de Exemplos Compartilhados", conforme sugerido por @Andrius aqui . Para coisas simples para as quais você não deseja passar pelo problema de ter vários arquivos, etc., veja como garantir o máximo controle sobre a visibilidade de suas coisas falsas (testado com o rspec 2.14.6, basta copiar e colar o código em um arquivo arquivo spec e execute-o):

module YourCoolModule
  def your_cool_module_method
  end
end

describe YourCoolModule do
  context "cntxt1" do
    let(:dummy_class) do
      Class.new do
        include YourCoolModule

        #Say, how your module works might depend on the return value of to_s for
        #the extending instances and you want to test this. You could of course
        #just mock/stub, but since you so conveniently have the class def here
        #you might be tempted to use it?
        def to_s
          "dummy"
        end

        #In case your module would happen to depend on the class having a name
        #you can simulate that behaviour easily.
        def self.name
          "DummyClass"
        end
      end
    end

    context "instances" do
      subject { dummy_class.new }

      it { subject.should be_an_instance_of(dummy_class) }
      it { should respond_to(:your_cool_module_method)}
      it { should be_a(YourCoolModule) }
      its (:to_s) { should eq("dummy") }
    end

    context "classes" do
      subject { dummy_class }
      it { should be_an_instance_of(Class) }
      it { defined?(DummyClass).should be_nil }
      its (:name) { should eq("DummyClass") }
    end
  end

  context "cntxt2" do
    it "should not be possible to access let methods from anohter context" do
      defined?(dummy_class).should be_nil
    end
  end

  it "should not be possible to access let methods from a child context" do
    defined?(dummy_class).should be_nil
  end
end

#You could also try to benefit from implicit subject using the descbie
#method in conjunction with local variables. You may want to scope your local
#variables. You can't use context here, because that can only be done inside
#a describe block, however you can use Porc.new and call it immediately or a
#describe blocks inside a describe block.

#Proc.new do
describe "YourCoolModule" do #But you mustn't refer to the module by the
  #constant itself, because if you do, it seems you can't reset what your
  #describing in inner scopes, so don't forget the quotes.
  dummy_class = Class.new { include YourCoolModule }
  #Now we can benefit from the implicit subject (being an instance of the
  #class whenever we are describing a class) and just..
  describe dummy_class do
    it { should respond_to(:your_cool_module_method) }
    it { should_not be_an_instance_of(Class) }
    it { should be_an_instance_of(dummy_class) }
    it { should be_a(YourCoolModule) }
  end
  describe Object do
    it { should_not respond_to(:your_cool_module_method) }
    it { should_not be_an_instance_of(Class) }
    it { should_not be_an_instance_of(dummy_class) }
    it { should be_an_instance_of(Object) }
    it { should_not be_a(YourCoolModule) }
  end
#end.call
end

#In this simple case there's necessarily no need for a variable at all..
describe Class.new { include YourCoolModule } do
  it { should respond_to(:your_cool_module_method) }
  it { should_not be_a(Class) }
  it { should be_a(YourCoolModule) }
end

describe "dummy_class not defined" do
  it { defined?(dummy_class).should be_nil }
end
Timo
fonte
Por alguma razão, apenas subject { dummy_class.new }está funcionando. O caso com subject { dummy_class }não está funcionando para mim.
valk
6

meu trabalho recente, usando o mínimo de fiação possível

require 'spec_helper'

describe Module::UnderTest do
  subject {Object.new.extend(described_class)}

  context '.module_method' do
    it {is_expected.to respond_to(:module_method)}
    # etc etc
  end
end

Eu gostaria

subject {Class.new{include described_class}.new}

funcionou, mas não funciona (como no Ruby MRI 2.2.3 e no RSpec :: Core 3.3.0)

Failure/Error: subject {Class.new{include described_class}.new}
  NameError:
    undefined local variable or method `described_class' for #<Class:0x000000063a6708>

Obviamente, a classe_descrita não é visível nesse escopo.

Leif
fonte
6

Para testar seu módulo, use:

describe MyCoolModule do
  subject(:my_instance) { Class.new.extend(described_class) }

  # examples
end

Para secar algumas coisas que você usa em várias especificações, você pode usar um contexto compartilhado:

RSpec.shared_context 'some shared context' do
  let(:reused_thing)       { create :the_thing }
  let(:reused_other_thing) { create :the_thing }

  shared_examples_for 'the stuff' do
    it { ... }
    it { ... }
  end
end
require 'some_shared_context'

describe MyCoolClass do
  include_context 'some shared context'

  it_behaves_like 'the stuff'

  it_behaves_like 'the stuff' do
    let(:reused_thing) { create :overrides_the_thing_in_shared_context }
  end
end

Recursos:

Allison
fonte
0

você precisa simplesmente incluir seu módulo no seu arquivo de especificação mudule Test module MyModule def test 'test' end end end no seu arquivo de especificação RSpec.describe Test::MyModule do include Test::MyModule #you can call directly the method *test* it 'returns test' do expect(test).to eql('test') end end

mdlx
fonte
-1

Uma solução possível para testar o método do módulo, independente da classe que os incluirá

module moduleToTest
  def method_to_test
    'value'
  end
end

E especifique isso

describe moduleToTest do
  let(:dummy_class) { Class.new { include moduleToTest } }
  let(:subject) { dummy_class.new }

  describe '#method_to_test' do
    it 'returns value' do
      expect(subject.method_to_test).to eq('value')
    end
  end
end

E se você quiser testá-los, o shared_examples é uma boa abordagem

Nermin
fonte
Não fui eu quem votou contra você, mas sugiro que você substitua suas duas LET por subject(:module_to_test_instance) { Class.new.include(described_class) }. Caso contrário, não vejo nada de errado com sua resposta.
Allison
-1

Esse é um padrão recorrente, pois você precisará testar mais de um módulo. Por esse motivo, é mais do que desejável criar um auxiliar para isso.

Encontrei este post que explica como fazê-lo, mas estou lidando aqui, pois o site pode ser desativado em algum momento.

Isso é para evitar que as instâncias do objeto não implementem o método da instância:: qualquer erro que você receber ao tentar allowmétodos nodummy classe.

Código:

No spec/support/helpers/dummy_class_helpers.rb

module DummyClassHelpers

  def dummy_class(name, &block)
    let(name.to_s.underscore) do
      klass = Class.new(&block)

      self.class.const_set name.to_s.classify, klass
    end
  end

end

No spec/spec_helper.rb

# skip this if you want to manually require
Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f}

RSpec.configure do |config|
  config.extend DummyClassHelpers
end

Nas suas especificações:

require 'spec_helper'

RSpec.shared_examples "JsonSerializerConcern" do

  dummy_class(:dummy)

  dummy_class(:dummy_serializer) do
     def self.represent(object)
     end
   end

  describe "#serialize_collection" do
    it "wraps a record in a serializer" do
      expect(dummy_serializer).to receive(:represent).with(an_instance_of(dummy)).exactly(3).times

      subject.serialize_collection [dummy.new, dummy.new, dummy.new]
    end
  end
end
juliangonzalez
fonte