【问题标题】:Rspec advice for testing service objects用于测试服务对象的 Rspec 建议
【发布时间】:2014-07-05 16:49:52
【问题描述】:

我正在为涉及多个模型的服务对象编写 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倡导者推荐的。这只是感觉倒退。关于编写高性能但有用的测试有什么建议吗?

【问题讨论】:

  • 是的,您基本上是在对大部分实际代码进行存根,因此除非您实际从中删除行,否则该方法不会真正失败。鉴于对象的性质,我会在集成级别而不是单元测试更多地测试这种业务逻辑。您正在处理做不同事情的多个对象。真正阅读您的方法,这听起来像是一个功能规范。我很想知道您如何在应用程序中使用 MealServicer。但是不要担心使用那些实际的对象。为调用该方法时预期发生的情况编写测试

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


【解决方案1】:

这是 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

【讨论】:

    猜你喜欢
    • 2017-10-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多