【问题标题】:Testing modules in RSpecRSpec 中的测试模块
【发布时间】:2010-12-05 07:17:07
【问题描述】:

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

【问题讨论】:

    标签: ruby unit-testing rspec


    【解决方案1】:

    最直接的方式 =>

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

    或者,您可以使用您的模块扩展测试类:

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

    使用 'let' 比使用实例变量在 before(:each) 中定义虚拟类要好

    When to use RSpec let()?

    【讨论】:

    • 不错。这帮助我避免了类 ivars 跨越测试的各种问题。通过分配给常量来给类名。
    • @lulalala 不,这是一个超类:ruby-doc.org/core-2.0.0/Class.html#method-c-new 要测试模块,请执行以下操作:let(:dummy_class) { Class.new { include ModuleToBeTested } }
    • 好极了。我通常这样做:let(:class_instance) { (Class.new { include Super::Duper::Module }).new },这样我就得到了最常用于测试的实例变量。
    • 使用include 对我不起作用,但extendlet(:dummy_class) { Class.new { extend ModuleToBeTested } } 起作用
    • 偶数:subject(:instance) { Class.new.include(described_class).new }
    【解决方案2】:

    麦克说了什么。这是一个简单的例子:

    模块代码...

    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
    

    【讨论】:

    • 有什么原因你没有在 DummyClass 声明中 include Say 而不是调用 extend
    • grant-birchmeier,他正在 extend 进入类的实例,即在调用 new 之后。如果您在调用 new 之前这样做,那么您是对的,您将使用 include
    • 我将代码编辑得更简洁。 @dummy_class= Class.new { extend Say } 是测试模块所需的全部内容。我怀疑人们会更喜欢这样,因为我们开发人员通常不喜欢输入过多的内容。
    • @TimHarper 尝试过,但实例方法变成了类方法。想法?
    • 为什么要定义DummyClass 常量?为什么不只是@dummy_class = Class.new?现在你用不必要的类定义污染了你的测试环境。这个 DummyClass 是为您的每一个规范定义的,并且在下一个规范中,您决定使用相同的方法并重新打开 DummyClass 定义,它可能已经包含一些东西(尽管在这个简单的例子中,定义在现实生活中是严格空的用例很可能在某个时候添加了一些东西,然后这种方法变得危险。)
    【解决方案3】:

    对于可以单独测试或通过模拟类进行测试的模块,我喜欢以下内容:

    模块:

    module MyModule
      def hallo
        "hallo"
      end
    end
    

    规格:

    describe MyModule do
      include MyModule
    
      it { hallo.should == "hallo" }
    end
    

    劫持嵌套示例组似乎是错误的,但我喜欢简洁。有什么想法吗?

    【讨论】:

    • 我喜欢这个,很简单。
    • 可能会弄乱 rspec。我认为使用@metakungfu 描述的let 方法更好。
    • @Cort3z 您绝对需要确保方法名称不会发生冲突。只有当事情真的很简单时,我才会使用这种方法。
    • 由于名称冲突,这搞砸了我的测试套件。
    【解决方案4】:

    我在 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
    
    【解决方案5】:

    在我的脑海中,你能在你的测试脚本中创建一个虚拟类并将模块包含在其中吗?然后测试虚拟类的行为是否符合您的预期。

    编辑:如果,如 cmets 中所指出的,模块期望某些行为出现在它所混合的类中,那么我会尝试实现这些行为的假人。足以让模块乐于履行其职责。

    也就是说,当一个模块期望从它的宿主(我们说“宿主”?)类中获得很多东西时,我会对我的设计有点紧张 - 如果我还没有从基类继承或者可以'不要将新功能注入继承树,然后我想我会尽量减少模块可能具有的任何此类期望。我担心我的设计会开始出现一些令人不快的僵化。

    【讨论】:

    • 如果我的模块依赖于具有某些属性和行为的类怎么办?
    【解决方案6】:

    接受的答案是我认为的正确答案,但是我想添加一个示例如何使用 rpsecs shared_examples_forit_behaves_like 方法。我在代码 sn-p 中提到了一些技巧,但有关更多信息,请参阅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
    

    【讨论】:

      【解决方案7】:

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

      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
      

      资源:

      【讨论】:

        【解决方案8】:

        我最近的工作,尽可能少地使用硬接线

        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 在该范围内不可见。

        【讨论】:

          【解决方案9】:

          怎么样:

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

          【讨论】:

            【解决方案10】:

            我建议对于较大且使用频率较高的模块,应该选择@Andrius here 所建议的“共享示例组”。对于您不想遇到多个文件等麻烦的简单内容,以下是如何确保最大程度地控制虚拟内容的可见性(使用 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 } 的情况对我不起作用。
            【解决方案11】:

            你也可以使用辅助类型

            # 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
            

            这是文档:https://www.relishapp.com/rspec/rspec-rails/v/3-3/docs/helper-specs/helper-spec

            【讨论】:

              【解决方案12】:

              您只需将您的模块包含到您的规范文件中 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

              【讨论】:

                【解决方案13】:

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

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

                【讨论】:

                • 我不是反对你的人,但我建议用subject(:module_to_test_instance) { Class.new.include(described_class) } 替换你的两个 LET。否则我真的看不出你的回答有什么问题。
                【解决方案14】:

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

                我发现 this post 解释了如何操作,但我在这里应付,因为该网站可能会在某个时候被删除。

                这是为了避免 object instances do not implement the instance method: :whatever 在尝试 allow 类上的 allow 方法时遇到的错误。

                代码:

                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
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2012-06-13
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2016-10-17
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多