Что такое метод "Saff Squeeze" для поиска ошибки?

Я прочитал оригинальный блог Kent Beck post по методу Saff Squeeze. Я также прочитал этот пост InfoQ, который более подробно описывает эту тему, но не содержит никаких примеров.

Я знаю, что это, по сути, способ прибегнуть к ошибке, не полагаясь на отладчик. Однако я считаю, что пример Кента не так ясен.

Может ли кто-нибудь более просвещенный просвещать меня о том, как использовать этот подход с ясным, конкретным примером? Он, мы надеемся, послужит учебным ресурсом для всех, кто также изучает этот метод.

Ответ 1

Saff Squeeze - это систематическая методика удаления как тестового кода, так и не тестового кода из неудачного теста, пока тест и код не станут достаточно маленькими для понимания.

Я согласен с тем, что первоначальное описание Кента Saff Squeeze Кента немного сложнее, отчасти потому, что тестируемое им программное обеспечение, JUnit, сильно абстрагировано, а отчасти потому, что он не приводит достаточного количества примеров шага 2: "Поместите (проваливающееся) утверждение ранее". в тесте, чем существующие утверждения. "

В своем первом раунде он просто поднимает утверждение выше в тесте, и его краткое изложение последующих шагов может привести вас к мысли, что единственное, что вы можете сделать на шаге 2, это переместить существующие утверждения, но своим последним шагом он придумает новое, более простое несостоятельное утверждение. Утверждение на шаге 2 может быть просто существующим, которое поднялось выше в тесте, что является обычным делом, но оно также может быть новым, которое вы придумали, когда понимаете код и эволюцию ошибки.

Вот пример. Это слишком просто, чтобы нуждаться в Saff Squeeze, но это иллюстрирует технику.

Я только что написал этот критически важный класс:

class Autopilot

  def self.fly_to(city)
    runways_in_city = runways_in city
    runway = closest_available runways_in_city
    flight_plan = flight_plan_to runway
    carry_out flight_plan
  end

  def self.runways_in(city)
    Airport.where(city: city).map(&:runways).flatten
  end

  def self.closest_available(runways)
    runways.select { |r| r.available? }
      .sort_by { |r| distance_between current_position, r.position }.last
  end

  def self.flight_plan_to(runway)
    FlightPlan.new runway.latitude, runway.longitude
  end

  # other methods left to the imagination

end

Вот первый пример rspec, который я написал, чтобы проверить это:

describe Autopilot
  describe ".fly_to" do
    it "flies to the only available runway" do
      Autopilot.stub(:current_position) { Position.new 0, 0 }
      nearby_runway = create :runway, latitude: 1, longitude: 1
      create :runway, city: nearby_runway.city, latitude: 2, longitude: 2
      flight_plan = FlightPlan.new nearby_runway.latitude, nearby_runway.longitude
      # Think of the following line as being at the end of the example, since that when it takes effect
      Autopilot.should_receive(:carry_out).with flight_plan
      Autopilot.fly_to nearby_runway.airport.city
    end
  end
end

О нет - последняя строка завершается с этим сообщением: "Ожидание не выполнено: ожидается, что Autopilot.carry_out будет вызван с FlightPlan (широта: 1, долгота: 1), но он был вызван с FlightPlan (широта: 2, долгота: 2) ". Я понятия не имею, как это произошло. Нам лучше использовать Saff Squeeze.

Вставьте метод (переименование локального, чтобы избежать конфликта имен):

it "flies to the only available runway" do
  Autopilot.stub(:current_position) { Position.new 0, 0 }
  nearby_runway = create :runway, latitude: 1, longitude: 1
  create :runway, city: nearby_runway.city, latitude: 2, longitude: 2
  flight_plan = FlightPlan.new nearby_runway.latitude, nearby_runway.longitude
  Autopilot.should_receive(:carry_out).with flight_plan
  runways_in_city = runways_in city
  runway = closest_available runways_in_city
  actual_flight_plan = flight_plan_to runway
  Autopilot.carry_out actual_flight_plan
end

Я не понимаю, как эта последняя строка может не соответствовать ожиданиям, если она получит правильный FlightPlan. Давайте посмотрим, сможем ли мы написать ошибочное утверждение выше в тесте:

it "flies to the only available runway" do
  Autopilot.stub(:current_position) { Position.new 0, 0 }
  nearby_runway = create :runway, latitude: 1, longitude: 1
  create :runway, city: nearby_runway.city, latitude: 2, longitude: 2
  flight_plan = FlightPlan.new nearby_runway.latitude, nearby_runway.longitude
  Autopilot.should_receive(:carry_out).with flight_plan
  runways_in_city = runways_in city
  runway = closest_available runways_in_city
  actual_flight_plan = flight_plan_to runway
  actual_flight_plan.should == flight_plan
  Autopilot.carry_out actual_flight_plan
end

Ах, новое утверждение тоже терпит неудачу, с "ожидаемым FlightPlan (широта: 1, долгота: 1), но получило FlightPlan (широта: 2, долгота: 2)". ОК, давайте упростим тест:

it "flies to the only available runway" do
  Autopilot.stub(:current_position) { Position.new 0, 0 }
  nearby_runway = create :runway, latitude: 1, longitude: 1
  create :runway, city: nearby_runway.city, latitude: 2, longitude: 2
  flight_plan = FlightPlan.new nearby_runway.latitude, nearby_runway.longitude
  runways_in_city = runways_in city
  runway = closest_available runways_in_city
  actual_flight_plan = flight_plan_to runway
  actual_flight_plan.should == flight_plan
end

Мы куда-то добираемся, но я все еще не понимаю, что не так. Лучше Saff Сожми flight_plan_to раз, вставив flight_plan_to:

it "flies to the only available runway" do
  Autopilot.stub(:current_position) { Position.new 0, 0 }
  nearby_runway = create :runway, latitude: 1, longitude: 1
  create :runway, city: nearby_runway.city, latitude: 2, longitude: 2
  flight_plan = FlightPlan.new nearby_runway.latitude, nearby_runway.longitude
  runways_in_city = runways_in city
  runway = closest_available runways_in_city
  actual_flight_plan = FlightPlan.new runway.latitude, runway.longitude
  actual_flight_plan.should == flight_plan
end

Ну, очевидно, что это пройдет, пока flight_plan_to получит правильную взлетно-посадочную полосу. Позвольте утверждать, что:

it "flies to the only available runway" do
  Autopilot.stub(:current_position) { Position.new 0, 0 }
  nearby_runway = create :runway, latitude: 1, longitude: 1
  create :runway, city: nearby_runway.city, latitude: 2, longitude: 2
  flight_plan = FlightPlan.new nearby_runway.latitude, nearby_runway.longitude
  runways_in_city = runways_in city
  runway = closest_available runways_in_city
  runway.should == nearby_runway
  actual_flight_plan = FlightPlan.new runway.latitude, runway.longitude
  actual_flight_plan.should == flight_plan
end

Хорошо, новое утверждение не выполнено, с "ожидаемой ВПП (id: 1), но полученной ВПП (id: 2)". Упростим тест еще раз:

it "flies to the only available runway" do
  Autopilot.stub(:current_position) { Position.new 0, 0 }
  nearby_runway = create :runway, latitude: 1, longitude: 1
  create :runway, city: nearby_runway.city, latitude: 2, longitude: 2
  runways_in_city = runways_in city
  runway = closest_available runways_in_city
  runway.should == nearby_runway
end

Мы сократили наш исходный тест и код до такой степени, что становится очевидным, что ошибка находится в closest_available - ее следует использовать first а не last.

Но что, если это все еще не очевидно, говорите вы? Хорошо, давайте попробуем Saff Squeeze снова, вставив closest_available:

it "flies to the only available runway" do
  Autopilot.stub(:current_position) { Position.new 0, 0 }
  nearby_runway = create :runway, latitude: 1, longitude: 1
  create :runway, city: nearby_runway.city, latitude: 2, longitude: 2
  runways_in_city = runways_in city
  runway = runways_in_city.select { |r| r.available? }
    .sort_by { |r| Autopilot.distance_between Autopilot.current_position, r.position }.last
  runway.should == nearby_runway
end

Теперь, где я собираюсь поместить ошибочное утверждение выше в тесте? Я не могу - ошибка находится в самой последней строке теста. В конце концов я буду вынужден осознать, что он был в closest_available прежде чем я добавлю это.

Ответ 2

Образец показывает, что он копирует (встраивает) код под тестом inline в свой unit test. Затем тестирование частей кода происходит отдельно от начала до конца. Это позволяет ему тестировать каждый путь изолированно и производить unit test на наименьших возможных единицах. Один из тестов продемонстрирует дефект, и вы сможете исправить свой недостаток. Образец, который он показывает, зависит от способности Eclipse к встроенным методам, если у вас нет этого, вам нужно сделать это вручную (копирование вызываемого кода на ваш unit test).