Rspec:如何期望mock在`each`循环中接收一个方法

时间:2014-04-17 15:01:33

标签: ruby-on-rails testing rspec

我需要测试这样的东西(简化):

class Event
  def self.charge_unpaid_events
    unpaid_events.each do |event|
      event.charge
    end
  end
  ...
end

我正在使用FactoryGirl并进行如下测试:

event = FactoryGirl.create(:event)
event.should receive(:charge)
Event.charge_unpaid_events

这失败了。我之前遇到过这个问题,我的理解是在每个循环的上下文中,块参数event是一个与我的模拟对象event不同的对象。我在过去通过确保只有一个事件实例并使用Event.any_instance.should receive...来解决这个问题,但这在我正在尝试测试的上下文中不起作用(出于其他原因我不会去成)。

这种限制是否有更明智的方法?

1 个答案:

答案 0 :(得分:1)

很难从这个小代码片段中分辨出来,但我猜测unpaid_eventsscope或类似的查询。此外,Event是某种DB支持"传统" Rails-y模型。

根据这些假设并给出了当前编写方法的方式,我通常会说这应该是一个验收规范。有了这种规格,我不会嘲笑任何东西。然而,听起来charge可能会产生一些影响深远且不良的副作用。

如果确实如此,那么在不更改代码的情况下,您需要存根Event.unpaid_events来返回一组模拟。通常,对被测对象进行存根是一种代码气味,并指向一个做得太多的对象。

以下是这样的规范可能是什么样的:

it "charges all unpaid events" do
  mock_events = [ double(Event, charge: true), double(Event, charge: true) ]
  allow(Event).to receive(:unpaid_events).and_return(mock_events)

  Event.charge_unpaid_events

  mock_events.all?{ |mock| expect(mock).to have_received(:charge) }
end

我可能会做的第一个重构是注入应该收取的事件,默认范围。我可能同时只收取那些无偿的事件:

class Event
  def self.charge_unpaid_events(events = unpaid_events)
    events.select(&:unpaid?)
          .each(&:charge)
  end
end

it "charges all the unpaid events" do
  paid_events = [ double(Event, unpaid: false), double(Event, unpaid: false) ]
  unpaid_events = [
    double(Event, unpaid: true, charge: true),
    double(Event, unpaid: true, charge: true)
  ]
  mock_events = paid_events + unpaid_events

  Event.charge_unpaid_events mock_events

  unpaid_events.all?{ |event| expect(event).to have_received(:charge) }
end

it "does not charge paid events" do
  # Normally testing 'not' conditions is more work than necessary, however for a
  # simple method such as this; plus the fact that charging things that are
  # `paid` can have more drastic consequences; I would argue this deserves a spec
  paid_events = [ double(Event, unpaid: false), double(Event, unpaid: false) ]
  unpaid_events = [
    double(Event, unpaid: true, charge: true),
    double(Event, unpaid: true, charge: true)
  ]
  mock_events = paid_events + unpaid_events

  Event.charge_unpaid_events mock_events

  paid_events.all?{ |event| expect(event).not_to have_received(:charge) }
end

此时我会质疑Event的工作是否负责管理收费以及应收取的费用。我可能会调查将这些内容移到外部服务类中,以便从一个简单的事件对象中收取责任。