Test dei moduli in rspec


175

Quali sono le migliori pratiche per testare i moduli in rspec? Ho alcuni moduli che vengono inclusi in alcuni modelli e per ora ho semplicemente dei test duplicati per ogni modello (con poche differenze). C'è un modo per asciugarlo?

Risposte:


219

Il modo rad = =

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

In alternativa puoi estendere la classe di test con il tuo modulo:

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

Usare 'let' è meglio che usare una variabile di istanza per definire la classe dummy nel precedente (: each)

Quando utilizzare RSpec let ()?


1
Bello. Questo mi ha aiutato a evitare ogni tipo di problema con gli ivar di classe che abbracciavano i test. Ha dato i nomi delle classi assegnando alle costanti.
capitano

3
@lulalala No, è una super classe: ruby-doc.org/core-2.0.0/Class.html#method-c-new Per testare i moduli fai qualcosa del genere:let(:dummy_class) { Class.new { include ModuleToBeTested } }
Timo

26
Way rad. Di solito lo faccio: in let(:class_instance) { (Class.new { include Super::Duper::Module }).new }questo modo ottengo la variabile di istanza che viene spesso utilizzata per testare in qualsiasi modo.
Automatico,

3
usare includenon funziona per me ma lo extendfalet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W,

8
Anche radder:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne,

108

Cosa ha detto Mike. Ecco un esempio banale:

codice modulo ...

module Say
  def hello
    "hello"
  end
end

frammento di specifica ...

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

3
Qualche motivo che non hai include Sayinserito nella dichiarazione DummyClass invece di chiamare extend?
Grant Birchmeier,

2
grant-birchmeier, sta extendentrando nell'istanza della classe, cioè dopo che newè stato chiamato. Se lo facessi prima che newvenga chiamato, allora hai ragione che include
useresti

8
Ho modificato il codice per essere più conciso. @dummy_class = Class.new {extension Say} è tutto ciò che serve per testare un modulo. Ho il sospetto che la gente preferirà che come noi sviluppatori spesso non ci piace digitare più del necessario.
Tim Harper

@TimHarper Ho provato ma i metodi di istanza sono diventati metodi di classe. Pensieri?
Lulalala,

6
Perché dovresti definire la DummyClasscostante? Perché non solo @dummy_class = Class.new? Ora inquini l'ambiente di test con una definizione di classe non necessaria. Questa DummyClass è definita per ognuna delle tue specifiche e nella prossima specifica in cui decidi di utilizzare lo stesso approccio e riaprire la definizione di DummyClass potrebbe già contenere qualcosa (sebbene in questo esempio banale la definizione sia rigorosamente vuota, nella vita reale casi d'uso è probabile che qualcosa venga aggiunto ad un certo punto e quindi questo approccio diventa pericoloso.)
Timo

29

Per i moduli che possono essere testati da soli o prendendo in giro la classe, mi piace qualcosa sulla falsariga di:

modulo:

module MyModule
  def hallo
    "hallo"
  end
end

spec:

describe MyModule do
  include MyModule

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

Potrebbe sembrare sbagliato dirottare gruppi di esempi nidificati, ma mi piace la terseness. qualche idea?


1
Mi piace questo, è così semplice.
Iain

2
Potrebbe rovinare l'spec. Penso che usare il letmetodo descritto da @metakungfu sia migliore.
Automatico,

@ Cort3z Devi assolutamente assicurarti che i nomi dei metodi non si scontrino. Sto usando questo approccio solo quando le cose sono davvero semplici.
Frank C. Schuetz,

Questo ha incasinato la mia suite di test a causa della collisione dei nomi.
roxxypoxxy,

24

Ho trovato una soluzione migliore nella homepage di rspec. Apparentemente supporta gruppi di esempi condivisi. Da https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples !

Gruppi di esempi condivisi

È possibile creare gruppi di esempi condivisi e includerli in altri gruppi.

Supponi di avere un comportamento che si applica a tutte le edizioni del tuo prodotto, sia grandi che piccole.

Innanzitutto, estrapola il comportamento "condiviso":

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

quindi quando è necessario definire il comportamento per le edizioni Large e Small, fare riferimento al comportamento condiviso utilizzando il metodo it_should_behave_like ().

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


21

In cima alla mia testa, potresti creare una classe fittizia nel tuo script di test e includere il modulo in quello? Quindi prova che la classe fittizia ha il comportamento come ti aspetteresti.

EDIT: Se, come sottolineato nei commenti, il modulo prevede che alcuni comportamenti siano presenti nella classe in cui è mescolato, allora proverei a implementare i manichini di quei comportamenti. Quanto basta per rendere felice il modulo per svolgere i suoi compiti.

Detto questo, sarei un po 'nervoso per il mio progetto quando un modulo si aspetta molto dalla sua classe host (diciamo "host"?) - Se non eredito già da una classe base o non riesco ad iniettare la nuova funzionalità nella struttura ereditaria quindi penso che avrei cercato di minimizzare tali aspettative che un modulo potrebbe avere. La mia preoccupazione è che il mio design inizierebbe a sviluppare alcune aree di spiacevole inflessibilità.


Cosa succede se il mio modulo dipende dalla classe che ha determinati attributi e comportamenti?
Andrius,

10

La risposta accettata è la risposta giusta penso, tuttavia ho voluto aggiungere un esempio su come usare rpsecs shared_examples_fore it_behaves_likemetodi. Cito alcuni trucchi nello snippet di codice ma per maggiori informazioni consulta questa guida relishapp-rspec .

Con questo puoi testare il tuo modulo in una qualsiasi delle classi che lo includono. Quindi stai davvero testando ciò che usi nella tua applicazione.

Vediamo un esempio:

# 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

Ora creiamo le specifiche per il nostro modulo: 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

6

Che dire:

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

6

Suggerirei che per moduli più grandi e molto usati si dovrebbe optare per i "Gruppi di esempi condivisi" come suggerito da @Andrius qui . Per cose semplici per le quali non vuoi affrontare il problema di avere più file, ecc. Ecco come garantire il massimo controllo sulla visibilità dei tuoi oggetti fittizi (testato con rspec 2.14.6, basta copiare e incollare il codice in un file spec ed eseguirlo):

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

Per qualche ragione funziona solo subject { dummy_class.new }. Il caso con subject { dummy_class }non funziona per me.
Valk,

6

il mio lavoro recente, usando il meno cablaggio possibile

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

spero che

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

ha funzionato, ma non funziona (come in Ruby MRI 2.2.3 e 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>

Ovviamente descritto_classe non è visibile in tale ambito.


6

Per testare il modulo, utilizzare:

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

  # examples
end

Per ASCIUGARE alcune cose che usi su più specifiche, puoi usare un contesto condiviso:

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

risorse:



0

devi semplicemente includere il tuo modulo nel tuo file spec mudule Test module MyModule def test 'test' end end end nel tuo file spec 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


-1

Una possibile soluzione per testare il metodo del modulo che sono indipendenti dalla classe che li includerà

module moduleToTest
  def method_to_test
    'value'
  end
end

E le specifiche per questo

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 vuoi DRY testarli, shared_examples è un buon approccio


Non sono stato io a sottovalutarti, ma suggerisco di sostituire le tue due LET con subject(:module_to_test_instance) { Class.new.include(described_class) }. Altrimenti non vedo davvero nulla di sbagliato nella tua risposta.
Allison,

-1

Questo è un modello ricorrente poiché dovrai testare più di un modulo. Per questo motivo, è più che desiderabile creare un aiuto per questo.

Ho trovato questo post che spiega come farlo, ma sto affrontando qui poiché il sito potrebbe essere rimosso a un certo punto.

Questo per evitare che le istanze dell'oggetto non implementino il metodo di istanza:: qualunque errore si verifichi quando si tenta di utilizzare allowmetodidummy classe.

Codice:

Nel 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

Nel 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

Nelle tue specifiche:

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
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.