Test des modules dans rspec

175

Quelles sont les meilleures pratiques pour tester les modules dans rspec? J'ai quelques modules qui sont inclus dans quelques modèles et pour l'instant j'ai simplement des tests en double pour chaque modèle (avec quelques différences). Y a-t-il un moyen de le SÉCHER?

Andrius
la source

Réponses:

219

La voie rad =>

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

Vous pouvez également étendre la classe de test avec votre module:

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

Utiliser 'let' est mieux que d'utiliser une variable d'instance pour définir la classe factice dans le before (: each)

Quand utiliser RSpec let ()?

metakungfu
la source
1
Agréable. Cela m'a aidé à éviter toutes sortes de problèmes avec les tests couvrant les ivars de classe. A donné les noms de classes en attribuant des constantes.
captainpete
3
@lulalala Non, c'est une super classe: ruby-doc.org/core-2.0.0/Class.html#method-c-new Pour tester les modules, faites quelque chose comme ceci:let(:dummy_class) { Class.new { include ModuleToBeTested } }
Timo
26
Way rad. J'ai l'habitude de faire:, de let(:class_instance) { (Class.new { include Super::Duper::Module }).new }cette façon, j'obtiens la variable d'instance qui est le plus souvent utilisée pour tester de toute façon.
Automatico
3
l'utilisation includene fonctionne pas pour moi mais le extendfaitlet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W
8
Encore plus radder:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne
108

Ce que Mike a dit. Voici un exemple trivial:

code du module ...

module Say
  def hello
    "hello"
  end
end

fragment de spécification ...

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
la source
3
Une raison pour laquelle vous ne l'avez pas fait include Saydans la déclaration DummyClass au lieu d'appeler extend?
Grant Birchmeier
2
grant-birchmeier, il est extenddans l'instance de la classe, c'est-à-dire après newavoir été appelé. Si vous faisiez cela avant newest appelé, vous avez raison, vous utiliseriezinclude
Hedgehog
8
J'ai édité le code pour être plus concis. @dummy_class = Class.new {extend Say} est tout ce dont vous avez besoin pour tester un module. Je soupçonne que les gens préféreront cela, car nous, les développeurs, n'aimons souvent pas taper plus que nécessaire.
Tim Harper
@TimHarper Essayé mais les méthodes d'instance sont devenues des méthodes de classe. Pensées?
lulalala le
6
Pourquoi définiriez-vous la DummyClassconstante? Pourquoi pas juste @dummy_class = Class.new? Maintenant, votre environnement de test pollue avec une définition de classe inutile. Cette DummyClass est définie pour chacune de vos spécifications et dans la spécification suivante où vous décidez d'utiliser la même approche et de rouvrir la définition DummyClass, elle peut déjà contenir quelque chose (bien que dans cet exemple trivial, la définition soit strictement vide, dans la vraie vie cas d'utilisation, il est probable que quelque chose soit ajouté à un moment donné, puis cette approche devient dangereuse.)
Timo
29

Pour les modules qui peuvent être testés isolément ou en se moquant de la classe, j'aime quelque chose du genre:

module:

module MyModule
  def hallo
    "hallo"
  end
end

spec:

describe MyModule do
  include MyModule

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

Il peut sembler incorrect de détourner des groupes d'exemples imbriqués, mais j'aime la concision. Des pensées?

Frank C. Schuetz
la source
1
J'aime ça, c'est tellement simple.
iain
2
Pourrait gâcher le rspec. Je pense que l'utilisation de la letméthode décrite par @metakungfu est meilleure.
Automatico
@ Cort3z Vous devez absolument vous assurer que les noms de méthodes ne se heurtent pas. J'utilise cette approche uniquement lorsque les choses sont vraiment simples.
Frank C. Schuetz
Cela a gâché ma suite de tests en raison d'une collision de noms.
roxxypoxxy
24

J'ai trouvé une meilleure solution sur la page d'accueil de rspec. Apparemment, il prend en charge les groupes d'exemples partagés. De https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples !

Groupes d'exemples partagés

Vous pouvez créer des exemples de groupes partagés et inclure ces groupes dans d'autres groupes.

Supposons que vous ayez un comportement qui s'applique à toutes les éditions de votre produit, grandes et petites.

Tout d'abord, supprimez le comportement «partagé»:

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

puis lorsque vous avez besoin de définir le comportement des éditions Large et Small, référencez le comportement partagé en utilisant la méthode 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
la source
21

Pourriez-vous créer une classe factice dans votre script de test et y inclure le module? Vérifiez ensuite que la classe factice a le comportement que vous attendez.

EDIT: Si, comme indiqué dans les commentaires, le module s'attend à ce que certains comportements soient présents dans la classe dans laquelle il est mélangé, alors j'essaierais d'implémenter des mannequins de ces comportements. Juste assez pour que le module soit heureux d'accomplir ses tâches.

Cela dit, je serais un peu nerveux à propos de ma conception lorsqu'un module attend beaucoup de sa classe hôte (disons-nous "hôte"?) - Si je n'hérite pas déjà d'une classe de base ou si je ne peux pas injecter la nouvelle fonctionnalité dans l'arborescence d'héritage alors je pense que j'essaierais de minimiser les attentes de ce type qu'un module pourrait avoir. Ma préoccupation étant que ma conception commence à développer des zones d'inflexibilité désagréable.

Mike Woodhouse
la source
Et si mon module dépend de la classe ayant certains attributs et comportements?
Andrius
10

La réponse acceptée est la bonne réponse, je pense, mais je voulais ajouter un exemple sur la façon d'utiliser les rpsecs shared_examples_foret les it_behaves_likeméthodes. Je mentionne quelques astuces dans l'extrait de code, mais pour plus d'informations, consultez ce relishapp-rspec-guide .

Avec cela, vous pouvez tester votre module dans l'une des classes qui l'incluent. Vous testez donc vraiment ce que vous utilisez dans votre application.

Voyons un exemple:

# 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

Créons maintenant les spécifications de notre module: 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
la source
6

Qu'en est-il de:

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

Je suggérerais que pour les modules plus grands et très utilisés, optez pour les "Groupes d'exemples partagés" comme suggéré par @Andrius ici . Pour les trucs simples pour lesquels vous ne voulez pas avoir la peine d'avoir plusieurs fichiers, etc. voici comment assurer un contrôle maximal sur la visibilité de vos trucs factices (testé avec rspec 2.14.6, copiez et collez simplement le code dans un spec et exécutez-le):

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
la source
Pour une raison quelconque, cela subject { dummy_class.new }fonctionne. Le cas avec subject { dummy_class }ne fonctionne pas pour moi.
valk
6

mon travail récent, en utilisant le moins de câblage possible

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

je souhaite que

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

a fonctionné, mais ce n'est pas le cas (comme à Ruby MRI 2.2.3 et 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>

La description_class n'est évidemment pas visible dans cette étendue.

Leif
la source
6

Pour tester votre module, utilisez:

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

  # examples
end

Pour SÉCHER certaines choses que vous utilisez sur plusieurs spécifications, vous pouvez utiliser un contexte partagé:

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

Ressources:

Allison
la source
0

vous devez simplement inclure votre module dans votre fichier de spécifications mudule Test module MyModule def test 'test' end end end dans votre fichier de spécifications 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
la source
-1

Une solution possible pour tester la méthode des modules qui sont indépendants de la classe qui les inclura

module moduleToTest
  def method_to_test
    'value'
  end
end

Et spec pour ça

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

Et si vous voulez les tester à sec , alors shared_examples est une bonne approche

Nermin
la source
Ce n'est pas moi qui vous ai décliné, mais je suggère de remplacer vos deux LET par subject(:module_to_test_instance) { Class.new.include(described_class) }. Sinon, je ne vois vraiment rien de mal à votre réponse.
Allison le
-1

Il s'agit d'un modèle récurrent car vous allez devoir tester plus d'un module. Pour cette raison, il est plus que souhaitable de créer une aide pour cela.

J'ai trouvé ce post qui explique comment le faire, mais je me débrouille ici car le site pourrait être supprimé à un moment donné.

C'est pour éviter que les instances d'objet n'implémentent la méthode d'instance:: quelle que soit l' erreur que vous obtenez lorsque vous essayez d' allowutiliser des méthodes sur la dummyclasse.

Code:

Dans 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

Dans 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

Dans vos spécifications:

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
la source