RSpec 中的测试模块

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

79661 次浏览

你能在你的测试脚本中创建一个虚拟类并把模块包含进去吗?然后测试虚拟类的行为是否符合您的预期。

编辑: 如果正如注释中指出的那样,模块期望它所混合的类中存在某些行为,那么我将尝试实现这些行为的模拟。只要能让模块高兴地执行任务就够了。

也就是说,当一个模块需要从它的主机(我们说“主机”吗?)得到很多东西时,我会对我的设计感到有点紧张Class-如果我还没有从基类继承,或者不能将新功能注入到继承树中,那么我想我会尽量减少模块可能具有的任何这样的期望。我担心的是,我的设计会开始发展一些令人不快的僵化的领域。

Mike 说的,举个小例子:

模块代码。

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

我在 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 _ act _ like ()方法。

describe "SmallEdition" do
it_should_behave_like "all editions"
it "should also behave like a small edition" do
end
end

拉德的方式 = >

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

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

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

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

何时使用 RSpec let () ?

对于那些可以单独测试或通过模拟类来测试的模块,我喜欢这样的东西:

模组:

module MyModule
def hallo
"hallo"
end
end

规格:

describe MyModule do
include MyModule


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

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

我认为公认的答案是正确的,但是我想添加一个如何使用 rpsecs shared_examples_forit_behaves_like方法的示例。我在代码片段中提到了一些技巧,但是更多的信息请参见这个 指南

有了这个,您可以在包含它的任何类中测试您的模块

让我们看一个例子:

# 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

现在让我们为模块创建 spec: 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 MyModule do
subject { Object.new.extend(MyModule) }
it "does stuff" do
expect(subject.does_stuff?).to be_true
end
end

我建议,对于更大和更常用的模块,应该选择@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

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

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>

显然 description _ class 在该作用域中不可见。

您还可以使用 helper 类型

# 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

您只需将您的模块包含到您的规范文件中 泥粒测试法泥粒测试法 模块 清晰度测试 测试 结束 结束 结束 在你的规格文件里 描述 Test: : MyModule 你可以直接调用方法 * Test * 它’返回测试’做 期望(测试) 结束 结束

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

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

如果你想干燥测试他们,那么 分享 _ 示例是一个很好的方法

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

我发现 这篇文章解释如何做到这一点,但我在这里应付,因为该网站可能会在某个时候关闭。

这是为了避免在 dummy类上尝试 allow方法时出现的 对象实例不实现实例方法: : whatever 错误。

密码:

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

要测试模块,请使用:

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

资源: