在 RSpec 中测试模块的最佳实践是什么?我有一些模块包含在几个模型中,现在我只是对每个模型进行重复测试(几乎没有差异)。有没有办法把它弄干?
激进的方式=>
let(:dummy_class) { Class.new { include ModuleToBeTested } }
或者,您可以使用您的模块扩展测试类:
let(:dummy_class) { Class.new { extend ModuleToBeTested } }
使用 'let' 比使用实例变量在 before(:each) 中定义虚拟类更好
迈克说了什么。这是一个简单的例子:
模块代码...
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
include Say
而不是调用 extend
的任何原因?
extend
进入类的实例,即在 new
被调用之后。如果您在调用 new
之前这样做,那么您是对的,您将使用 include
DummyClass
常量?为什么不只是 @dummy_class = Class.new
?现在你用不必要的类定义污染了你的测试环境。这个 DummyClass 是为您的每一个规范定义的,并且在下一个规范中,您决定使用相同的方法并重新打开 DummyClass 定义,它可能已经包含一些东西(尽管在这个简单的例子中,定义在现实生活中是严格空的用例很可能在某个时候添加了一些东西,然后这种方法变得危险。)
对于可以单独测试或通过模拟类进行测试的模块,我喜欢以下内容:
模块:
module MyModule
def hallo
"hallo"
end
end
规格:
describe MyModule do
include MyModule
it { hallo.should == "hallo" }
end
劫持嵌套的示例组似乎是错误的,但我喜欢简洁。有什么想法吗?
let
方法更好。
我在 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
在我的脑海中,你能在你的测试脚本中创建一个虚拟类并将模块包含在其中吗?然后测试虚拟类是否具有您期望的行为。
编辑:如果,正如评论中所指出的,模块期望某些行为出现在它所混合的类中,那么我会尝试实现这些行为的假人。足以让模块愉快地履行其职责。
也就是说,当一个模块对它的宿主(我们说“宿主”?)类有很多期望时,我会对我的设计有点紧张 - 如果我还没有从基类继承或无法注入将新功能添加到继承树中,然后我想我会尽量减少模块可能具有的任何此类期望。我担心我的设计会开始发展一些令人不快的僵化领域。
接受的答案是我认为的正确答案,但是我想添加一个示例如何使用 rpsecs shared_examples_for
和 it_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
要测试您的模块,请使用:
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
资源:
共享示例
共享上下文
更好的规格
我最近的工作,尽可能少地使用硬接线
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 在该范围内不可见。
关于什么:
describe MyModule do
subject { Object.new.extend(MyModule) }
it "does stuff" do
expect(subject.does_stuff?).to be_true
end
end
我建议对于更大和更常用的模块,应该选择@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 }
的情况对我不起作用。
你也可以使用辅助类型
# 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
您只需将模块包含到您的规范文件 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
测试模块方法的一种可能解决方案,该方法独立于包含它们的类
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。否则我真的看不出你的回答有什么问题。
这是一种反复出现的模式,因为您将需要测试多个模块。出于这个原因,为此创建一个助手是非常可取的。
我发现 this post 解释了如何做,但我在这里应付,因为该网站可能会在某个时候被删除。
这是为了避免 object instances do not implement the instance method: :whatever 错误,您在尝试使用 dummy
类的 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
不定期副业成功案例分享
let(:dummy_class) { Class.new { include ModuleToBeTested } }
let(:class_instance) { (Class.new { include Super::Duper::Module }).new }
,这样我就得到了最常用于测试的实例变量。include
对我不起作用,但extend
对let(:dummy_class) { Class.new { extend ModuleToBeTested } }
subject(:instance) { Class.new.include(described_class).new }