6

自从我开始使用 rspec 以来,我一直对固定装置的概念有疑问。我主要担心的是:

  1. 我使用测试来揭示令人惊讶的行为。对于我正在测试的示例,我并不总是足够聪明地列举所有可能的边缘情况。使用硬编码的固定装置似乎是有限的,因为它只用我想象的非常具体的情况来测试我的代码。(诚​​然,我的想象力也限制了我测试的案例。)

  2. 我使用测试作为代码文档的一种形式。如果我有硬编码的夹具值,则很难揭示特定测试试图演示的内容。例如:

    describe Item do
      describe '#most_expensive' do
        it 'should return the most expensive item' do
          Item.most_expensive.price.should == 100
          # OR
          #Item.most_expensive.price.should == Item.find(:expensive).price
          # OR
          #Item.most_expensive.id.should == Item.find(:expensive).id
        end
      end
    end
    

    使用第一种方法不会让读者知道最贵的物品是什么,只是它的价格是 100。这三种方法都要求读者相信灯具:expensive是列出的最贵的灯具fixtures/items.yml。粗心的程序员可能会通过创建一个Iteminbefore(:all)或将另一个夹具插入到fixtures/items.yml. 如果这是一个大文件,可能需要很长时间才能找出问题所在。

我开始做的一件事是#generate_random为我的所有模型添加一个方法。此方法仅在我运行规范时可用。例如:

class Item
  def self.generate_random(params={})
    Item.create(
      :name => params[:name] || String.generate_random,
      :price => params[:price] || rand(100)
    )
  end
end

(我如何做到这一点的具体细节实际上更简洁一些。我有一个处理所有模型的生成和清理的类,但这段代码对于我的示例来说已经足够清晰了。)所以在上面的示例中,我可以测试为跟随。对虚心的警告:我的代码严重依赖于使用before(:all)

describe Item do
  describe '#most_expensive' do
    before(:all) do
      @items = []
      3.times { @items << Item.generate_random }
      @items << Item.generate_random({:price => 50})
    end

    it 'should return the most expensive item' do
      sorted = @items.sort { |a, b| b.price <=> a.price }
      expensive = Item.most_expensive
      expensive.should be(sorted[0])
      expensive.price.should >= 50      
    end
  end
end

这样,我的测试可以更好地揭示令人惊讶的行为。当我以这种方式生成数据时,我偶尔会偶然发现我的代码没有按预期运行的边缘情况,但如果我只使用固定装置,我就不会发现这种情况。例如,在 的情况下#most_expensive,如果我忘记处理多个项目共享最贵价格的特殊情况,我的测试偶尔会在第一次失败should。看到 AutoSpec 中的非确定性故障会提示我出了点问题。如果我只使用固定装置,发现这样的错误可能需要更长的时间。

我的测试在代码中演示预期的行为方面也做得更好。我的测试清楚地表明 sorted 是按价格降序排序的项目数组。由于我希望#most_expensive等于该数组的第一个元素,因此更明显的是预期的行为most_expensive是什么。

那么,这是一种不好的做法吗?我对固定装置的恐惧是非理性的吗?为每个模型写一个generate_random方法是不是工作量太大了?或者这行得通吗?

4

12 回答 12

14

我对这个主题或Jason Baker提到 的蒙特卡洛测试没有人感到惊讶。那是我唯一一次广泛使用随机测试输入。但是,通过为每个测试用例的随机数生成器设置一个恒定的种子,使测试具有可重复性非常重要。

于 2009-03-11T21:15:09.567 回答
5

我们在我最近的一个项目中考虑了很多。最后,我们确定了两点:

  • 测试用例的可重复性至关重要。如果您必须编写随机测试,请准备好对其进行大量记录,因为如果/当它失败时,您将需要确切知道原因。
  • 使用随机性作为代码覆盖率的拐杖意味着您要么没有很好的覆盖率,要么您对领域的了解不足以知道什么构成了代表性测试用例。找出哪个是正确的并相应地修复它。

总之,随机性往往比它的价值更麻烦。在扣动扳机之前,请仔细考虑是否要正确使用它。我们最终决定,随机测试用例一般来说是个坏主意,并且要谨慎使用,如果有的话。

于 2009-03-11T21:11:31.213 回答
5

这是对您第二点的回答:

(2) 我使用测试作为代码文档的一种形式。如果我有硬编码的夹具值,则很难揭示特定测试试图演示的内容。

我同意。理想情况下,规范示例本身应该是可以理解的。使用固定装置是有问题的,因为它将示例的先决条件与其预期结果分开。

正因为如此,许多 RSpec 用户已经完全停止使用固定装置。相反,在规范示例本身中构造所需的对象。

describe Item, "#most_expensive" do
  it 'should return the most expensive item' do
    items = [
      Item.create!(:price => 100),
      Item.create!(:price => 50)
    ]

    Item.most_expensive.price.should == 100
  end
end

如果您最终获得了大量用于创建对象的样板代码,您应该查看许多测试对象工厂库中的一些,例如factory_girlMachinistFixtureReplacement

于 2009-03-12T07:39:19.380 回答
2

已经发布了很多好的信息,但另请参阅:模糊测试。大街上的消息是,微软在他们的很多项目中都使用了这种方法。

于 2009-03-11T21:16:29.703 回答
1

我的测试经验主要是用 C/Python/Java 编写的简单程序,所以我不确定这是否完全适用,但是每当我有一个可以接受任何类型用户输入的程序时,我总是包含一个测试随机输入数据,或者至少是计算机以不可预测的方式生成的输入数据,因为您永远无法假设用户将输入什么。或者,好吧,你可以,但是如果你这样做了,那么一些没有做出这种假设的黑客很可能会发现一个你完全忽略的错误。机器生成的输入是我所知道的将人为偏见完全排除在测试程序之外的最好(唯一?)方法。当然,为了重现失败的测试,您必须在运行测试之前执行一些操作,例如将测试输入保存到文件或打印出来(如果是文本)。

于 2009-03-11T21:50:20.810 回答
1

只要您没有解决预言机问题的方法,即根据输入确定软件的预期结果,随机测试就是一种不好的做法。

如果你解决了预言机问题,你可以比简单的随机输入生成更进一步。您可以选择输入分布,以便软件的特定部分得到比简单随机更多的锻炼。

然后,您从随机测试切换到统计测试。

if (a > 0)
    // Do Foo
else (if b < 0)
    // Do Bar
else
    // Do Foobar

如果你在范围内随机选择a,你有50% 的时间、25% 的时间和25% 的时间锻炼。您可能会发现 in 中的错误多于或中的错误。bintFooBarFoobarFooBarFoobar

如果你选择a这样它在 66.66% 的时间是负的,Bar并且Foobar比你的第一个分布得到更多的锻炼。事实上,三个分支各有 33.33% 的时间被执行。

当然,如果您观察到的结果与您的预期结果不同,您必须记录所有对重现错误有用的内容。

于 2009-03-11T21:50:48.017 回答
1

我建议看看机械师:

http://github.com/notahat/machinist/tree/master

机械师会为您生成数据,但它是可重复的,因此每次测试运行都有相同的随机数据。

您可以通过一致地播种随机数生成器来做类似的事情。

于 2009-03-12T02:57:36.170 回答
1

使用随机测试数据是一种极好的实践——硬编码的测试数据只测试您明确想到的情况,而随机数据会清除您可能错误的隐含假设。

我强烈建议为此使用 Factory Girl 和 ffaker。(在任何情况下都不要使用 Rails 固定装置。)

于 2012-02-13T21:02:11.283 回答
0

随机生成的测试用例的一个问题是验证答案应该由代码计算,你不能确定它没有错误:)

于 2009-03-11T21:06:19.980 回答
0

您可能还会看到以下主题: 使用随机输入进行测试的最佳实践

于 2009-03-11T21:08:12.063 回答
0

此类测试的有效性在很大程度上取决于您使用的随机数生成器的质量以及将 RNG 的输出转换为测试数据的代码的正确程度。

如果 RNG 永远不会产生导致您的代码进入某些极端情况的值,那么您将不会涵盖这种情况。如果您将 RNG 的输出转换为您测试的代码的输入的代码有缺陷,那么即使使用好的生成器,您仍然可能无法达到所有边缘情况。

你将如何测试?

于 2009-03-12T07:53:31.093 回答
0

测试用例中随机性的问题在于输出是随机的。

测试(尤其是回归测试)背后的想法是检查是否没有任何问题。

如果您发现有问题,您需要从那时起每次都包含该测试,否则您将无法获得一致的测试集。此外,如果您运行一个有效的随机测试,那么您需要包含该测试,因为您可能会破坏代码以使测试失败。

换句话说,如果您有一个使用动态生成的随机数据的测试,我认为这是一个坏主意。但是,如果您使用一组随机数据,然后存储和重用,这可能是一个好主意。这可以采取随机数生成器的一组种子的形式。

生成数据的这种存储使您可以找到对此数据的“正确”响应。

因此,我建议使用随机数据来探索您的系统,但在测试中使用定义的数据(最初可能是随机生成的数据)

于 2009-03-12T08:13:29.907 回答