查找错误的"安全挤压"方法究竟是什么?



我读过Kent Beck关于staff Squeeze方法的博文。我也读过InfoQ上的一篇文章,它详细阐述了这个话题,但没有提供任何例子。

我知道它本质上是一种在不依赖调试器的情况下定位bug的方法。然而,我发现肯特的例子并不是那么清楚。

有没有更开明的人可以用一个清晰、具体的例子来教我如何使用这种方法?希望它也能成为研究该方法的其他人的学习资源。

staff Squeeze是一种系统的技术,用于从失败的测试中删除测试代码和非测试代码,直到测试和代码小到足以理解。

我同意Kent最初对staff Squeeze的描述有点困难,部分原因是他正在测试的软件JUnit是高度抽象的,部分原因是他没有给出足够的步骤2的例子,"在测试中放置一个(失败的)断言,而不是现有的断言。"

在他的第一轮中,他只是移动了测试中更高的断言,他对后面步骤的总结可能会让您认为在第2步中唯一可以做的就是移动现有的断言,但是在他的最后一步中,他提出了一个新的,更简单的失败断言。第2步中的断言可以只是在测试中移动到较高位置的现有断言,这很常见,但它也可以是随着您对代码和bug的理解的发展而产生的新断言。

这里有一个例子。不需要 staff 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's when it takes effect
      Autopilot.should_receive(:carry_out).with flight_plan
      Autopilot.fly_to nearby_runway.airport.city
    end
  end
end

哦,不——最后一行失败了,这条消息是:"预期失败:预期自动驾驶。"carry_out被FlightPlan(latitude: 1, longitude: 1)调用,但它被FlightPlan(latitude: 2, longitude: 2)调用。我不知道这是怎么回事。我们最好使用staff 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

我们有所进展,但我还是不知道哪里不对。更好的工作人员再次挤压,内联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

但是如果你说它仍然不明显呢?好吧,让我们再试一次staff 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

现在,我要把失败的断言放在测试的哪个位置呢?不行,bug在测试的最后一行。最后我不得不意识到,在我内联它之前,它是在closest_available中。

示例显示他正将测试中的代码复制(内联)到单元测试中。然后从头到尾分别测试代码的各个部分。这使他能够独立地测试每个路径,并在尽可能小的单元上进行生产单元测试。其中一个测试将演示缺陷,您将能够修复缺陷。他展示的示例依赖于Eclipse内联方法的能力,如果没有这种能力,则需要手工完成(将被调用的代码复制到单元测试中)。

这本质上是对代码树的深度搜索,通过遍历单元测试中的代码树来找到bug。然后修剪任何不包含错误的分支。

我怀疑如果代码包含多个问题导致观察到的(错误)行为,则可能存在问题。

如果你在你的代码树中得到一个带有大量子分支的节点(一个很长的方法),我更喜欢使用启发式二进制搜索来确定bug所在:)

相关内容

最新更新