如何用RSpec模拟范围内的any_instance?

时间:2013-03-13 00:17:33

标签: ruby-on-rails ruby rspec mocking bdd

我正在尝试编写一个规范,该规范要求在作用域中的所有实例上调用方法。无法找到一种优雅的方式来做到这一点。

这是我的代码的简化表示:

class MyClass < ActiveRecord::Base

scope :active, where(:status => 'active')
scope :inactive, where(:status => 'inactive')

def some_action
  # some code
end

此类由另一个在some_action上调用MyClass.all的类使用:

class OtherClass

def other_method
  MyClass.all.each do |item|
    item.some_action
  end
end

我想将其更改为:

class OtherClass

def other_method
  MyClass.active.each do |item|
    item.some_action
  end
end

为了测试这种行为,我可以简单地MyClass.stub(:active),返回一个存根数组,并期望每个存根上有some_action。但我不喜欢这种方法,因为它暴露了太多的实现细节。

我更喜欢的东西更像是any_instance_in_scope。然后我可以简单地将我的规范写成:

MyClass.any_instance_in_scope(:active).should_receive(:some_action)

有没有办法实现这个目标?

1 个答案:

答案 0 :(得分:4)

首先,MyClass.all.some_action不起作用,因为MyClass#some_action是实例方法,同时MyClass#all返回Array - 所以当你执行MyClass.all.some_action时1}}你实际上是在呼叫Array#some_action

另外,请注意MyClass.allMyClass.active返回不同的类:

MyClass.active.class # => ActiveRecord::Relation
MyClass.active.all.class # => Array

我不确定你的some_action应该做什么......我想你可能想做的一些选择:

选项#1:缩小数据库查询

如果some_action正在过滤数组,则应将其转换为另一个范围,执行以下操作:

class MyClass < ActiveRecord::Base
  scope :active, where(:status => 'active')
  scope :inactive, where(:status => 'inactive')
  scope :some_action, ->(color_name) { where(color: color_name) }
end

然后使用MyClass.active.some_action('red').all调用它。如果您只想要第一个结果MyClass.active.some_action('red').first

如何使用RSpec

测试scope

这是一个很好的答案(及其原因):Testing named scopes with RSpec

选项#2:在实例顶部执行操作

假设您确实希望将MyClass#some_action定义为实例方法。然后,您可以尝试这样做:

class MyClass < ActiveRecord::Base
  scope :active, where(status: 'active')
  scope :inactive, where(status: 'inactive')

  def some_action
    self.foo = 'bar'
    self
  end
end

在这种情况下,您可以使用MyClass.active.last.some_action执行它,因为#last将返回一个实例,而不是整个数组。

如何使用RSpec

测试some_action

我相信你应该只是按照期望进行测试:

MyClass.should_receive(:some_action).at_least(:once)
MyClass.active.last.some_action

关于此问题的进一步讨论:How to say any_instance should_receive any number of times in RSpec

选项#3:群众行动

假设你真的想要运行MyClass.active.some_action。我建议你先试试这个(与选项#2相同的例子):

class MyClass < ActiveRecord::Base
  scope :active, where(status: 'active')
  scope :inactive, where(status: 'inactive')

  def some_action
    self.foo = 'bar'
    self
  end
end

然后使用MyClass.active.all.map{|my_class| my_class.some_action }运行。

现在,如果确实想要实现MyClass.active.some_action - 您希望在ActiveRecord :: Relation的所有实例上执行some_action我不喜欢不推荐),执行此操作:

class MyClass < ActiveRecord::Base
  scope :active, where(status: 'active')
  scope :inactive, where(status: 'inactive')

  def some_action
    # really do it
  end
end

和...

class ActiveRecord::Relation
  # run some_action over all instances
  def some_action
    to_a.each {|object| object.some_action }.tap { reset }
  end
end

同样,我不建议这样做

如何使用RSpec

测试some_action

与选项#2相同:

MyClass.should_receive(:some_action).at_least(:once)
MyClass.active.last.some_action

注意:所有代码都使用Ruby 2.0.0-p0。安装并使用它,很有趣! : - )