web-dev-qa-db-fra.com

Test des modules dans rspec

Quelles sont les meilleures pratiques pour tester des 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 DRY en place?

160
Andrius

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

Exemples de groupes 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, grand et petit.

Tout d’abord, excluez le «partagé» comportement: 

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

puis, quand vous aurez besoin de définir le comportement pour les grandes et petites éditions, référence 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
23
Andrius

La voie rad =>

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

Sinon, vous pouvez étendre la classe de test avec votre module: 

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

Utiliser 'let' est préférable à l’utilisation d’une variable d’instance pour définir la classe fictive à l’avant

Quand utiliser RSpec let ()?

195
metakungfu

Quel micro a dit. Voici un exemple trivial:

code du module ...

module Say
  def hello
    "hello"
  end
end

fragment de spec ...

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
107
Karmen Blake

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

module:

module MyModule
  def hallo
    "hallo"
  end
end

spec:

describe MyModule do
  include MyModule

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

Cela peut sembler inopportun de pirater des exemples de groupes imbriqués, mais j'aime bien la concision. Des pensées?

29
Frank C. Schuetz

De mémoire, pourriez-vous créer une classe fictive dans votre script de test et inclure le module dans celui-ci? Testez ensuite que la classe factice a le comportement attendu.

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é, j'essaierais alors d'implémenter des variables factices de ces comportements. Juste de quoi rendre le module heureux dans l’accomplissement de ses tâches.

Cela dit, je serais un peu inquiet à propos de ma conception lorsqu'un module attend beaucoup de sa classe Host (disons-nous "hôte"?) - Si je n'hérite pas déjà d'une classe de base ou ne peux pas injecter Je pense que la nouvelle fonctionnalité dans l’arborescence de l’héritage essaierait donc de minimiser les attentes de ce type. Mon souci est que ma conception commence à développer des zones de rigidité désagréable.

21
Mike Woodhouse

La réponse acceptée est la bonne, mais je voulais ajouter un exemple d'utilisation des méthodes rpsecs shared_examples_for et it_behaves_like. Je mentionne quelques astuces dans l'extrait de code, mais pour plus d'informations, voir ceci relishapp-rspec-guide

Avec cela, vous pouvez tester votre module dans n’importe laquelle 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 des spécifications pour 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
9
p1100i

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
6
Matt Connolly

Je suggérerais que, pour les modules les plus volumineux et les plus utilisés, on opte pour les "Groupes d'exemples partagés" proposés par @Andrius ici . Pour des choses simples pour lesquelles vous ne voulez pas avoir à vous soucier de fichiers multiples, etc., voici comment vous assurer un contrôle maximal sur la visibilité de vos choses factices 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
6
Timo

mes travaux récents, 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 cela ne fonctionne pas (comme dans 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>

De toute évidence, Classe_classe n'est pas visible dans cette étendue.

5
Leif

Vous pouvez également utiliser le type d'assistance

# api_helper.rb
module Api
  def my_meth
    10
  end
end
# spec/api_spec.rb
require "api_helper"

RSpec.describe Api, :type => :helper do
  describe "#my_meth" do
    it { expect( helper.my_meth ).to eq 10 }
  end
end

Voici la documentation: https://www.relishapp.com/rspec/rspec-Rails/v/3-3/docs/helper-specs/helper-spec

3
Uri

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

0
mdlx