rspecでのモジュールのテスト


175

rspecでモジュールをテストする際のベストプラクティスは何ですか?いくつかのモデルに含まれるモジュールがいくつかありますが、今のところは、各モデルのテストが重複しているだけです(違いはほとんどありません)。乾かす方法はありますか?

回答:


219

radの方法=>

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

または、モジュールでテストクラスを拡張することもできます。

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

「let」を使用する方が、インスタンス変数を使用してbefore(:each)でダミークラスを定義するよりも優れています

RSpec let()を使用する場合?


1
いいね。これは、クラスivarsスパンテストに関するあらゆる種類の問題を回避するのに役立ちました。定数に割り当てることでクラス名を付けます。
captainpete 2012年

3
ノー@lulalala、それはスーパークラスです:ruby-doc.org/core-2.0.0/Class.html#method-c-newへのテストモジュールは、このような何か:let(:dummy_class) { Class.new { include ModuleToBeTested } }
ティモ

26
ウェイラド。私は通常、次のことを行いますlet(:class_instance) { (Class.new { include Super::Duper::Module }).new }。そのようにして、何らかの方法でテストするために最も頻繁に使用されるインスタンス変数を取得します。
Automatico

3
を使用includeしても動作しませんが、extend動作しますlet(:dummy_class) { Class.new { extend ModuleToBeTested } }
Mike W

8
はしごでも:subject(:instance) { Class.new.include(described_class).new }
Richard-Degenne

108

マイクが言ったこと。これは簡単な例です:

モジュールコード...

module Say
  def hello
    "hello"
  end
end

スペックフラグメント...

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
include Say呼び出す代わりにDummyClass宣言の内部にいなかった理由はextend何ですか?
Grant Birchmeier、2012

2
grant-birchmeier extendは、クラスのインスタンスにアクセスしていますnew。つまり、呼び出された後です。あなたがこれnewが呼び出される前にこれを行っていたなら、あなたはあなたが使うだろう正しいinclude
Hedgehog

8
より簡潔になるようにコードを編集しました。@dummy_class = Class.new {extend Say}は、モジュールをテストするために必要なすべてです。私たちの開発者は必要以上にタイプするのを好まないことが多いので、人々はそれを好むと思います。
Tim Harper、2013年

@TimHarper試しましたが、インスタンスメソッドはクラスメソッドになりました。考え?
lulalala 2013年

6
なぜDummyClass定数を定義するのですか?なぜ@dummy_class = Class.newですか?これで、不要なクラス定義でテスト環境を汚染しています。このDummyClassは、仕様のすべてに対して定義され、同じアプローチを使用してDummyClass定義を再度開くことを決定する次の仕様では、すでに何かが含まれている可能性があります(この簡単な例では、定義は実際には厳密に空ですが)ユースケースでは、ある時点で何かが追加され、このアプローチが危険になる可能性があります。)
Timo

29

単独で、またはクラスをモックすることによってテストできるモジュールの場合、私は次のように好きです:

モジュール:

module MyModule
  def hallo
    "hallo"
  end
end

スペック:

describe MyModule do
  include MyModule

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

ネストされたサンプルグループをハイジャックするのは間違っているように見えるかもしれませんが、私は簡潔さを気に入っています。何かご意見は?


1
私はこれが好きです、それはとても簡単です。
13

2
rspecを台無しにするかもしれません。let@metakungfuで説明されている方法を使用する方が良いと思います。
Automatico

@ Cort3zメソッド名が衝突しないようにする必要があります。このアプローチは、物事が非常に単純な場合にのみ使用します。
フランクC.シュエッツ2014年

これは名前の衝突のために私のテストスイートを台無しにしました。
roxxypoxxy

24

私はrspecのホームページでより良い解決策を見つけました。どうやら、共有されたサンプルグループをサポートしています。https://www.relishapp.com/rspec/rspec-core/v/2-13/docs/example-groups/shared-examplesから!

共有サンプルグループ

共有サンプルグループを作成し、それらのグループを他のグループに含めることができます。

大小を問わず、製品のすべてのエディションに適用される動作があるとします。

まず、「共有」動作を除外します。

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

次に、LargeエディションとSmallエディションの動作を定義する必要がある場合は、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

私の頭の上のところで、テストスクリプトにダミークラスを作成し、その中にモジュールを含めることができますか?次に、ダミークラスが期待どおりの動作をすることをテストします。

編集:コメントで指摘されているように、モジュールが混在するクラスにいくつかの動作が存在することをモジュールが期待している場合は、それらの動作のダミーを実装しようとします。モジュールがその任務を実行できるようにするのに十分です。

そうは言っても、モジュールがそのホスト(「ホスト」と言いますか?)クラスから多くのことを期待しているとき、私は設計について少し緊張します-ベースクラスからまだ継承していないか、注入できない場合継承ツリーに新しい機能を追加すると、モジュールが持つ可能性のあるそのような期待を最小限に抑えようとしていると思います。私の懸念は、私のデザインが不快な柔軟性を欠くいくつかの領域を発達させ始めるだろうということです。


モジュールが特定の属性と動作を持つクラスに依存している場合はどうなりますか?
Andrius

10

受け入れられた答えは正しい答えだと思いますが、rpsecs shared_examples_forit_behaves_likeメソッドの使用方法の例を追加したいと思いました。私はコードスニペットでいくつかのトリックに言及していますが、詳細については、このrelishapp-rspec-guideを参照してください。

これにより、モジュールを含むクラスのいずれかでモジュールをテストできます。つまり、実際にアプリケーションで使用するものをテストしています。

例を見てみましょう:

# 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

次に、モジュールの仕様を作成します。 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

何について:

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

6

ここで @Andriusによって提案されているように、より大きくて使用頻度の高いモジュールについては、「共有サンプルグループ」を選択することをお勧めします。複数のファイルを持つなどのトラブルを回避したい単純なものの場合は、ダミーのものの可視性を最大限に制御する方法を次に示します(rspec 2.14.6でテストし、コードをコピーして、スペックファイルとそれを実行):

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

何らかの理由でのみ機能しsubject { dummy_class.new }ています。のケースでsubject { dummy_class }はうまくいきません。
2014

6

私の最近の仕事、可能な限り少ない配線を使用

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

私は望む

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

機能しましたが、機能しません(Ruby MRI 2.2.3および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>

もちろん、described_classはそのスコープでは表示されません。


6

モジュールをテストするには、以下を使用します。

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

  # examples
end

複数の仕様で使用するいくつかのものを乾燥させるには、共有コンテキストを使用できます。

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

リソース:



0

あなたは単にあなたのスペックファイル mudule Test module MyModule def test 'test' end end end にあなたのスペックファイルにあなたのモジュールを含める必要があり ます 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

それらを含むクラスに依存しないモジュールメソッドをテストするための1つの可能なソリューション

module moduleToTest
  def method_to_test
    'value'
  end
end

そしてそれのための仕様

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

そして、それらをDRYテストしたい場合は、shared_examplesが良いアプローチです


あなたに反対票を投じたのは私ではありませんでしたが、2つのLETをに置き換えることをお勧めしますsubject(:module_to_test_instance) { Class.new.include(described_class) }。それ以外の場合、私はあなたの答えに何も問題はないと思います。
アリソン

-1

複数のモジュールをテストする必要があるため、これは繰り返し発生するパターンです。そのため、このためのヘルパーを作成することは望ましいことではありません。

その方法を説明するこの投稿を見つけましたが、サイトがいつか停止される可能性があるため、ここで対処しています。

これは回避することですオブジェクトインスタンスは、インスタンスメソッドを実装していない:何でもしようとした際に表示エラーallowのメソッドdummyクラス。

コード:

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

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

あなたのスペックでは:

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.