用于测试服务对象的Rspec建议

时间:2014-05-17 07:24:30

标签: ruby-on-rails ruby unit-testing rspec

我正在为一个接触多个模型的服务对象编写Rspec测试,但我觉得我的测试过于依赖于方法的内部,因此不是很有意义。这是一个例子:

class MealServicer

  def self.serve_meal(meal, customer)
    meal.update_attributes(status: "served", customer_id: customer.id)
    order = customer.order
    OrderServicer.add_meal_to_order(meal, order)
    CRM.update_customer_record(customer) // external API call
  end

end

我想使用双精度/存根来模拟行为而不实际保存测试数据库中的任何内容(性能)。但是如果我创建响应消息的双精度数,那么感觉就像我正在测试serve_meal()方法的一个特定实现,并且这个测试太过耦合到那个特定的实现。例如,我需要确保我的customer double响应order并返回order存根。基本上,当一切都只是一个双重的时候我必须通过确保双精度返回其他双精度来明确说明所有依赖关系,感觉测试最终变得毫无意义。见这里:

it "has a working serve_meal method" do
  meal = double(:meal)
  customer = double(:customer)
  order = double(:order)

  allow(customer).to_receive(:order).and_return(order)
  allow(OrderServicer).to_receive(:add_meal_to_order).and_return(true)
  allow(CRM).to_receive(:update_customer_record).and_return(true)

  expect(meal).to receive(:update_attributes).once
  expect(OrderServicer).to receive(:add_meal_to_order).once
  expect(CRM).to receive(:update_customer_record).once
end

除了实例化实际的膳食,客户和订单对象(可能保存到数据库)之外,还有其他方法可以彻底而有意义地进行测试,然后检查MealServicer.serve_meal(...)是否更新了对象属性如预期?这最终将最终保存到数据库,因为update_attributes执行了一次保存调用,因此我打算在我的Service对象方法中包含几个方法。

最后因为测试取决于实现,我不能在方法之前编写测试,这是TDD倡导者所推荐的。这只是感觉倒退。有关编写高效但有用的测试的建议吗?

1 个答案:

答案 0 :(得分:18)

这是Martin Fowler的Mocks Aren't Stubs中提到的'Mockist vs Classicist'困境。在整个过程中使用模拟(双打)必然需要在协作者上隐藏其他方法并公开实现。这是你为模拟的速度和灵活性付出的代价的一部分。

另一个问题是规范没有自然的“主题”,因为这是一种类方法。最终会得到三个需要更新的对象;从某种意义上说,它们是交替的主体和合作者,取决于正在进行的期望。您可以通过为每个示例设置一个期望来使此更清晰:

describe MealServicer do
  context ".serve_meal" do
    let(:order) { double(:order) }
    let(:meal) { double(:meal) }
    let(:customer) { double(:customer, id: 123, order: order }

    it "updates the meal" do
      allow(OrderServicer).to_receive(:add_meal_to_order)
      allow(CRM).to_receive(:update_customer_record)
      expect(meal).to receive(:update_attributes).with(status: "served", customer_id: 123)
      MealServicer.serve_meal(meal, customer)
    end

    it "adds the meal to the order" do
      allow(meal).to receive(:update_attributes)
      allow(CRM).to_receive(:update_customer_record)
      expect(OrderServicer).to receive(:add_meal_to_order).with(meal, order)
      MealServicer.serve_meal(meal, customer)
    end

    it "updates the customer record" do
      allow(meal).to receive(:update_attributes)
      allow(OrderServicer).to_receive(:add_meal_to_order)
      expect(CRM).to receive(:update_customer_record).with(customer)
      MealServicer.serve_meal(meal, customer)
    end
  end
end

现在存根总是依赖关系,期望是被测试的东西,这澄清了规范的意图。

因为测试依赖于实现,我无法在方法之前编写测试

我不同意。如果你将期望分开,那么你可以先测试并编写代码以使测试通过,如果你一次只做一个例子。

修改

另见Myron Marston的blog post