在RSpec中测试模块


174

在rspec中测试模块的最佳实践是什么?我有一些模块包含在少数几个模型中,现在我只对每个模型进行重复测试(几乎没有差异)。有办法干燥吗?

Answers:


218

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 } }
Timo

26
路拉德。我通常这样做:let(:class_instance) { (Class.new { include Super::Duper::Module }).new },这样我就可以获取以任何方式最常用于测试的实例变量。
Automatico

3
使用include我不工作,但extend确实let(:dummy_class) { Class.new { extend ModuleToBeTested } }
麦克W¯¯

8
甚至更合理:subject(:instance) { Class.new.include(described_class).new }
理查德·戴

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 SayDummyClass声明内而不是在调用的任何原因extend
Grant Birchmeier

2
Grant-birchmeier,他extend进入了该类的实例,即在new被调用之后。如果您在new打电话之前就这么做过,那么您会用对的include
刺猬2012年

8
我编辑了代码以使其更简洁。@dummy_class = Class.new {扩展说}是测试模块所需的全部。我怀疑人们会喜欢这种方式,因为我们开发人员经常不喜欢输入过多的文字。
蒂姆·哈珀

@TimHarper尝试过,但是实例方法变成了类方法。有什么想法吗?
lulalala

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
我喜欢这个,很简单。
iain

2
可能使rspec混乱。我认为使用let@metakungfu描述的方法更好。
Automatico

@ Cort3z您绝对需要确保方法名称不会冲突。仅当事情非常简单时,我才使用这种方法。
弗兰克·舒茨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

然后,当您需要定义大版和小版的行为时,请使用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

在我的脑海中,您可以在测试脚本中创建一个虚拟类并将模块包含到其中吗?然后测试虚拟类是否具有您期望的行为。

编辑:如果,如注释中所指出,该模块希望某些行为出现在混入该类的类中,那么我将尝试实现这些行为的虚拟模型。刚好足以使模块高兴地执行其职责。

就是说,当模块期望从其宿主获得很多东西(我们说“宿主”吗?)时,我会为我的设计感到有些紧张-如果我尚未从基类继承或无法注入然后将新功能添加到继承树中,那么我想我将尽量减少模块可能具有的任何此类期望。我担心的是,我的设计将开始发展一些令人不快的灵活性。


如果我的模块依赖于具有某些属性和行为的类怎么办?
安德里斯2009年

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测试),只需将代码复制并粘贴到spec文件并运行它):

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>

显然describe_class在该范围内不可见。



4

要测试您的模块,请使用:

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

一种独立于类的测试模块方法的可能解决方案,该方法将包括它们

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是一个很好的方法


我不是打败您的人,但我建议您用替换您的两个LET subject(:module_to_test_instance) { Class.new.include(described_class) }。否则,我的回答没有什么错。
艾里森

-1

这是一种循环模式,因为您将需要测试多个模块。出于这个原因,为此创建一个助手是非常可取的。

我找到了说明该操作方法的帖子,但由于该网站可能会在某个时候被关闭,因此我在这里应对。

这是为了避免对象实例不实现实例方法::尝试allowdummy类进行方法时遇到的任何错误。

码:

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.