Рекомендации 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

Я хотел бы использовать double/stubs для издевательства над тем, чтобы ничего не сохранять в тестовой базе данных (для повышения производительности). Но если я создаю дубликаты, отвечающие на сообщения, то мне кажется, что я тестирую одну конкретную реализацию метода 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 выполняет вызов сохранения, а также некоторые из методов, которые я намерен включить в мой метод объектов службы.

Наконец, поскольку тесты зависят от реализации, я не могу написать тесты перед методом, что рекомендуют сторонники TDD. Это просто отвратительно. Любые советы по написанию исполнителей, но полезные тесты?

Ответ 1

Это дилемма "Моккист против классицизма", адресованная Мартину Фаулеру Mocks Are not Stubs. Использование mocks (doubleles) повсюду обязательно потребует отработки других методов на коллаборационистах и ​​демонстрации реализации. Это часть цены, которую вы платите за скорость и гибкость насмешек.

Другая проблема заключается в том, что для спецификации нет естественного "субъекта", потому что это метод класса. Вы получаете три объекта, каждый из которых нуждается в обновлении; в некотором смысле они являются поочередно субъектами и сотрудниками в зависимости от того, какое ожидание осуществляется. Вы можете сделать это более понятным, установив одно ожидание на пример:

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