Testen von Modulen in rspec

175

Was sind die Best Practices zum Testen von Modulen in rspec? Ich habe einige Module, die in wenigen Modellen enthalten sind, und im Moment habe ich einfach doppelte Tests für jedes Modell (mit wenigen Unterschieden). Gibt es eine Möglichkeit, es auszutrocknen?

Andrius
quelle

Antworten:

219

Der rad Weg =>

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

Alternativ können Sie die Testklasse mit Ihrem Modul erweitern:

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

Die Verwendung von 'let' ist besser als die Verwendung einer Instanzvariablen zum Definieren der Dummy-Klasse in der vorherigen (: each)

Wann soll RSpec let () verwendet werden?

Metakung Fu
quelle
1
Nett. Dies hat mir geholfen, alle möglichen Probleme mit Klassen-Ivar-Tests zu vermeiden. Hat den Klassennamen durch Zuweisen zu Konstanten gegeben.
Captainpete
3
@lulalala Nein, es ist eine super Klasse: ruby-doc.org/core-2.0.0/Class.html#method-c-new Um Module zu testen, machen Sie so etwas:let(:dummy_class) { Class.new { include ModuleToBeTested } }
Timo
26
Weg rad. Normalerweise mache ich: Auf let(:class_instance) { (Class.new { include Super::Duper::Module }).new }diese Weise erhalte ich die Instanzvariable, die am häufigsten zum Testen verwendet wird.
Automatico
3
Verwenden includefunktioniert nicht für mich, aber extendfunktioniertlet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W
8
Noch radder:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne
108

Was Mike gesagt hat. Hier ist ein triviales Beispiel:

Modulcode ...

module Say
  def hello
    "hello"
  end
end

Spezifikationsfragment ...

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
quelle
3
include SayGibt es einen Grund, warum Sie nicht in der DummyClass-Deklaration waren, anstatt anzurufen extend?
Grant Birchmeier
2
Grant-Birchmeier, er tritt extendin die Instanz der Klasse ein, dh nachdem newer gerufen wurde. Wenn Sie dies getan haben, bevor newaufgerufen wird, dann haben Sie Recht, Sie würden verwendeninclude
Hedgehog
8
Ich habe den Code präziser bearbeitet. @dummy_class = Class.new {extens Say} ist alles, was Sie zum Testen eines Moduls benötigen. Ich vermute, die Leute werden das vorziehen, da wir Entwickler oft nicht mehr als nötig tippen möchten.
Tim Harper
@TimHarper Versucht, aber Instanzmethoden wurden zu Klassenmethoden. Gedanken?
Lulalala
6
Warum würden Sie die DummyClassKonstante definieren ? Warum nicht einfach @dummy_class = Class.new? Jetzt verschmutzen Sie Ihre Testumgebung mit einer unnötigen Klassendefinition. Diese DummyClass ist für jede einzelne Ihrer Spezifikationen definiert. In der nächsten Spezifikation, in der Sie denselben Ansatz verwenden und die DummyClass-Definition erneut öffnen, enthält sie möglicherweise bereits etwas (obwohl in diesem trivialen Beispiel die Definition im wirklichen Leben streng leer ist Anwendungsfälle Es ist wahrscheinlich, dass irgendwann etwas hinzugefügt wird und dieser Ansatz dann gefährlich wird.)
Timo
29

Für Module, die isoliert oder durch Verspotten der Klasse getestet werden können, gefällt mir Folgendes:

Modul:

module MyModule
  def hallo
    "hallo"
  end
end

spec:

describe MyModule do
  include MyModule

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

Es mag falsch erscheinen, verschachtelte Beispielgruppen zu entführen, aber ich mag die Knappheit. Irgendwelche Gedanken?

Frank C. Schütz
quelle
1
Ich mag das, es ist so einfach.
iain
2
Könnte die rspec durcheinander bringen. Ich denke, die letvon @metakungfu beschriebene Methode ist besser.
Automatico
@ Cort3z Sie müssen auf jeden Fall sicherstellen, dass Methodennamen nicht kollidieren. Ich benutze diesen Ansatz nur, wenn die Dinge wirklich einfach sind.
Frank C. Schuetz
Dies hat meine Testsuite aufgrund einer Namenskollision durcheinander gebracht.
Roxxypoxxy
24

Ich habe eine bessere Lösung auf der rspec-Homepage gefunden. Anscheinend unterstützt es gemeinsame Beispielgruppen. Von https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples !

Gemeinsame Beispielgruppen

Sie können freigegebene Beispielgruppen erstellen und diese Gruppen in andere Gruppen aufnehmen.

Angenommen, Sie haben ein Verhalten, das für alle Editionen Ihres Produkts gilt, sowohl für große als auch für kleine.

Berücksichtigen Sie zunächst das „gemeinsame“ Verhalten:

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

Wenn Sie dann das Verhalten für die großen und kleinen Editionen definieren müssen, verweisen Sie mit der Methode it_should_behave_like () auf das gemeinsame Verhalten.

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

Könnten Sie auf den ersten Blick eine Dummy-Klasse in Ihrem Testskript erstellen und das Modul in dieses aufnehmen? Testen Sie dann, ob die Dummy-Klasse das erwartete Verhalten aufweist.

BEARBEITEN: Wenn das Modul, wie in den Kommentaren erwähnt, erwartet, dass einige Verhaltensweisen in der Klasse vorhanden sind, in die es gemischt ist, würde ich versuchen, Dummies dieser Verhaltensweisen zu implementieren. Gerade genug, um das Modul glücklich zu machen, seine Aufgaben zu erfüllen.

Trotzdem wäre ich etwas nervös wegen meines Designs, wenn ein Modul eine ganze Menge von seiner Host-Klasse erwartet (sagen wir "Host"?) - Wenn ich nicht bereits von einer Basisklasse erbe oder nicht injizieren kann Die neue Funktionalität in den Vererbungsbaum, dann würde ich versuchen, solche Erwartungen, die ein Modul haben könnte, zu minimieren. Ich befürchte, dass mein Design einige Bereiche unangenehmer Inflexibilität entwickeln könnte.

Mike Woodhouse
quelle
Was ist, wenn mein Modul von einer Klasse mit bestimmten Attributen und Verhaltensweisen abhängt?
Andrius
10

Die akzeptierte Antwort ist meiner Meinung nach die richtige Antwort. Ich wollte jedoch ein Beispiel für die Verwendung von rpsecs shared_examples_forund it_behaves_likeMethoden hinzufügen . Ich erwähne einige Tricks im Code-Snippet, aber für weitere Informationen siehe diese relishapp-rspec-Anleitung .

Damit können Sie Ihr Modul in jeder der Klassen testen, die es enthalten. Sie testen also wirklich, was Sie in Ihrer Anwendung verwenden.

Sehen wir uns ein Beispiel an:

# 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

Jetzt erstellen wir eine Spezifikation für unser Modul: 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
quelle
6

Wie wäre es mit:

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

Ich würde vorschlagen, dass man sich für größere und häufig verwendete Module für die "Shared Example Groups" entscheiden sollte, wie von @Andrius hier vorgeschlagen . Für einfache Dinge, für die Sie nicht die Mühe haben möchten, mehrere Dateien usw. zu haben, erfahren Sie hier, wie Sie die maximale Kontrolle über die Sichtbarkeit Ihrer Dummy-Sachen gewährleisten (getestet mit rspec 2.14.6, kopieren Sie einfach den Code und fügen Sie ihn in ein ein Spezifikationsdatei und führen Sie es aus):

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
quelle
Aus irgendeinem Grund subject { dummy_class.new }funktioniert nur. Der Fall mit subject { dummy_class }funktioniert bei mir nicht.
Valk
6

meine jüngste Arbeit mit so wenig Kabel wie möglich

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

Ich wünsche

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

hat funktioniert, aber nicht (wie bei Ruby MRI 2.2.3 und 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>

Offensichtlich ist die beschriebene Klasse in diesem Bereich nicht sichtbar.

Leif
quelle
5

Verwenden Sie zum Testen Ihres Moduls:

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

  # examples
end

Um einige Dinge, die Sie in mehreren Spezifikationen verwenden, auszutrocknen, können Sie einen gemeinsamen Kontext verwenden:

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

Ressourcen:

Allison
quelle
0

Sie müssen Ihr Modul einfach in Ihre Spezifikationsdatei mudule Test module MyModule def test 'test' end end end in Ihre Spezifikationsdatei aufnehmen 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
quelle
-1

Eine mögliche Lösung zum Testen von Modulmethoden, die unabhängig von der Klasse sind, in der sie enthalten sind

module moduleToTest
  def method_to_test
    'value'
  end
end

Und spezifizieren Sie dafür

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

Und wenn Sie sie DRY testen möchten, ist shared_examples ein guter Ansatz

Nermin
quelle
Ich war nicht derjenige, der Sie herabgestimmt hat, aber ich schlage vor, Ihre beiden LETs durch zu ersetzen subject(:module_to_test_instance) { Class.new.include(described_class) }. Ansonsten sehe ich mit Ihrer Antwort nichts falsches.
Allison
-1

Dies ist ein wiederkehrendes Muster, da Sie mehr als ein Modul testen müssen. Aus diesem Grund ist es mehr als wünschenswert, einen Helfer dafür zu erstellen.

Ich habe diesen Beitrag gefunden , in dem erklärt wird, wie es geht, aber ich komme hier zurecht, da die Site möglicherweise irgendwann heruntergefahren wird.

Um zu vermeiden, dass die Objektinstanzen die Instanzmethode nicht implementieren :: welcher Fehler auch immer auftritt, wenn Sie versuchen, allowMethoden für die dummyKlasse zu verwenden.

Code:

Im 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

Im 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

In Ihren Spezifikationen:

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
quelle