Testowanie modułów w rspec

175

Jakie są najlepsze praktyki dotyczące testowania modułów w rspec? Mam kilka modułów, które są zawarte w kilku modelach i na razie po prostu mam zduplikowane testy dla każdego modelu (z kilkoma różnicami). Czy jest sposób, aby go WYSUSZAĆ?

Andrius
źródło

Odpowiedzi:

219

Rad sposób =>

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

Alternatywnie możesz rozszerzyć klasę testową o swój moduł:

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

Użycie „let” jest lepsze niż użycie zmiennej instancji do zdefiniowania fikcyjnej klasy w poprzednim (: each)

Kiedy używać RSpec let ()?

metakungfu
źródło
1
Miły. Pomogło mi to uniknąć wszelkiego rodzaju problemów z testami obejmującymi klasy IVAR. Nazwy klas nadał przez przypisanie do stałych.
captainpete
3
@lulalala Nie, to super klasa: ruby-doc.org/core-2.0.0/Class.html#method-c-new Aby przetestować moduły, zrób coś takiego:let(:dummy_class) { Class.new { include ModuleToBeTested } }
Timo
26
Way rad. Zwykle robię:, w let(:class_instance) { (Class.new { include Super::Duper::Module }).new }ten sposób otrzymuję zmienną instancji, która jest najczęściej używana do testowania w jakikolwiek sposób.
Automatico
3
używanie includenie działa dla mnie, ale extenddziałalet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W
8
Nawet radder:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne
108

Co powiedział Mike. Oto trywialny przykład:

kod modułu ...

module Say
  def hello
    "hello"
  end
end

fragment specyfikacji ...

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
źródło
3
Jest jakiś powód, dla którego nie include Sayznalazłeś się w deklaracji DummyClass zamiast dzwonić extend?
Grant Birchmeier
2
grant-birchmeier, extendwchodzi do instancji klasy, czyli po newwywołaniu. Jeśli robiłeś to wcześniej, newto masz rację, byś użyłinclude
Jeż
8
Zredagowałem kod, aby był bardziej zwięzły. @dummy_class = Class.new {extension Say} to wszystko, czego potrzebujesz, aby przetestować moduł. Podejrzewam, że ludzie wolą to, ponieważ my, programiści, często nie lubimy pisać więcej niż to konieczne.
Tim Harper
@TimHarper Wypróbowano, ale metody instancji stały się metodami klasowymi. Myśli?
lulalala
6
Dlaczego zdefiniowałbyś DummyClassstałą? Dlaczego nie po prostu @dummy_class = Class.new? Teraz zanieczyszczasz środowisko testowe niepotrzebną definicją klasy. Ta DummyClass jest zdefiniowana dla każdej z twoich specyfikacji, aw następnej specyfikacji, w której zdecydujesz się użyć tego samego podejścia i ponownie otworzyć definicję DummyClass, może już coś zawierać (chociaż w tym trywialnym przykładzie definicja jest całkowicie pusta, w prawdziwym życiu przypadków użycia jest prawdopodobne, że coś zostanie dodane w pewnym momencie, a potem to podejście stanie się niebezpieczne.)
Timo
29

W przypadku modułów, które można testować w izolacji lub przez wyśmiewanie klasy, podoba mi się coś w stylu:

moduł:

module MyModule
  def hallo
    "hallo"
  end
end

specyfikacja:

describe MyModule do
  include MyModule

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

Przejmowanie zagnieżdżonych grup przykładów może wydawać się niewłaściwe, ale podoba mi się zwięzłość. jakieś pomysły?

Frank C. Schuetz
źródło
1
Podoba mi się to, to takie proste.
innymi
2
Może zepsuć rspec. Myślę, że użycie letmetody opisanej przez @metakungfu jest lepsze.
Automatico,
@ Cort3z Zdecydowanie musisz się upewnić, że nazwy metod nie kolidują. Używam tego podejścia tylko wtedy, gdy sprawy są naprawdę proste.
Frank C. Schuetz,
To zepsuło mój zestaw testów z powodu kolizji nazw.
roxxypoxxy
24

Znalazłem lepsze rozwiązanie na stronie domowej rspec. Najwyraźniej obsługuje wspólne grupy przykładowe. Z https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examples !

Wspólne przykładowe grupy

Możesz tworzyć współużytkowane grupy przykładowe i dołączać je do innych grup.

Załóżmy, że masz pewne zachowanie, które dotyczy wszystkich wersji Twojego produktu, zarówno dużych, jak i małych.

Po pierwsze, weź pod uwagę „wspólne” zachowanie:

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

wtedy, gdy musisz zdefiniować zachowanie dla edycji Large i Small, odwołaj się do wspólnego zachowania za pomocą metody 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
źródło
21

Na samą myśl, czy mógłbyś utworzyć fikcyjną klasę w swoim skrypcie testowym i dołączyć do niej moduł? Następnie sprawdź, czy fikcyjna klasa zachowuje się w oczekiwany sposób.

EDYCJA: Jeśli, jak wskazano w komentarzach, moduł oczekuje pewnych zachowań w klasie, do której jest zmieszany, to spróbuję zaimplementować manekiny tych zachowań. Wystarczająco, aby moduł był szczęśliwy z wykonywania swoich obowiązków.

To powiedziawszy, byłbym trochę zdenerwowany moim projektem, gdy moduł oczekuje dużo od swojej klasy hosta (czy mówimy „host”?) - Jeśli nie dziedziczę już z klasy bazowej lub nie mogę wstrzyknąć nową funkcjonalność w drzewie dziedziczenia, myślę, że spróbuję zminimalizować wszelkie takie oczekiwania, jakie może mieć moduł. Martwię się, że mój projekt zacznie rozwijać pewne obszary nieprzyjemnej sztywności.

Mike Woodhouse
źródło
Co jeśli mój moduł zależy od klasy mającej określone atrybuty i zachowanie?
Andrius,
10

Zaakceptowana odpowiedź jest właściwą, myślę, ale chciałem dodać przykład, jak używać rpseców shared_examples_fori it_behaves_likemetod. Wspominam o kilku sztuczkach we fragmencie kodu, ale aby uzyskać więcej informacji, zobacz ten przewodnik relishapp-rspec .

Dzięki temu możesz przetestować swój moduł w dowolnej klasie, która go zawiera. Więc naprawdę testujesz to, czego używasz w swojej aplikacji.

Zobaczmy przykład:

# 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

Teraz stwórzmy specyfikację dla naszego modułu: 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
źródło
6

Co powiesz na:

describe MyModule do
  subject { Object.new.extend(MyModule) }
  it "does stuff" do
    expect(subject.does_stuff?).to be_true
  end
end
Matt Connolly
źródło
6

Sugerowałbym, aby w przypadku większych i często używanych modułów wybrać „Wspólne grupy przykładowe”, zgodnie z sugestią @Andrius tutaj . W przypadku prostych rzeczy, dla których nie chcesz mieć kłopotów z posiadaniem wielu plików itp., Oto jak zapewnić maksymalną kontrolę nad widocznością twoich atrap (testowane z rspec 2.14.6, po prostu skopiuj i wklej kod do pliku spec i uruchom go):

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
źródło
Tylko z jakiegoś powodu subject { dummy_class.new }działa. Sprawa z subject { dummy_class }nie działa dla mnie.
valk
6

moja ostatnia praca, przy użyciu jak najmniejszej ilości okablowania

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

chciałbym

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

zadziałało, ale nie działa (jak w Ruby MRI 2.2.3 i 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>

Oczywiście opisana klasa nie jest widoczna w tym zakresie.

Leif
źródło
6

Aby przetestować swój moduł, użyj:

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

  # examples
end

Aby wysuszyć niektóre rzeczy, których używasz w wielu specyfikacjach, możesz użyć wspólnego kontekstu:

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

Zasoby:

Allison
źródło
0

musisz po prostu dołączyć swój moduł do pliku mudule Test module MyModule def test 'test' end end end specyfikacji w pliku specyfikacji 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
źródło
-1

Jedno z możliwych rozwiązań do testowania metod modułów, które są niezależne od klasy, która je będzie zawierała

module moduleToTest
  def method_to_test
    'value'
  end
end

I specyfikację tego

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

A jeśli chcesz je przetestować na SUCHO , to shared_examples jest dobrym podejściem

Nermin
źródło
To nie ja cię przegłosowałem, ale proponuję zastąpić twoje dwa LET subject(:module_to_test_instance) { Class.new.include(described_class) }. W przeciwnym razie nie widzę nic złego w Twojej odpowiedzi.
Allison
-1

Jest to powtarzający się wzorzec, ponieważ będziesz musiał przetestować więcej niż jeden moduł. Z tego powodu tworzenie pomocnika do tego jest bardziej niż pożądane.

Znalazłem ten post, który wyjaśnia, jak to zrobić, ale radzę sobie tutaj, ponieważ witryna może zostać w pewnym momencie usunięta.

Ma to na celu uniknięcie sytuacji, w których instancje obiektu nie implementują metody instancji:: dowolny błąd, który otrzymasz podczas próby użycia allowmetod dummyklasy.

Kod:

W 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

W 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

W twoich specyfikacjach:

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
źródło