随机生成测试数据是不好的做法?

时间:2009-03-11 21:02:11

标签: ruby testing rspec fixtures

自从我开始使用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中列出。粗心的程序员可以通过在Item中创建before(:all)或通过在fixtures/items.yml中插入另一个工具来中断测试。如果这是一个大文件,可能需要很长时间才能找出问题所在。

  3. 我开始做的一件事是为我的所有模型添加#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方法工作太多了吗?或者这有效吗?

12 个答案:

答案 0 :(得分:14)

我很惊讶在这个主题或Jason Baker linked to提到的那个人中没有人 Monte Carlo Testing。这是我唯一一次广泛使用随机测试输入。但是,通过为每个测试用例提供随机数生成器的恒定种子,使测试可重现是非常重要的。

答案 1 :(得分:5)

我们在最近的一个项目中考虑过这个问题。最后,我们确定了两点:

  • 测试用例的可重复性至关重要。如果您必须编写随机测试,请准备好进行广泛的记录,因为如果/如果失败,您将需要确切知道原因。
  • 使用随机性作为代码覆盖的拐杖意味着您要么没有良好的覆盖范围,要么您不了解该领域,足以了解代表性测试用例的构成。弄清楚哪个是真的并相应地修复它。

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

答案 2 :(得分: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_girlMachinist或{{3} }。

答案 3 :(得分:2)

已经发布了很多好的信息,但另见:Fuzz Testing。街上的一句话是,微软在很多项目中使用这种方法。

答案 4 :(得分:1)

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

答案 5 :(得分:1)

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

如果你解决了oracle问题,你可以比简单的随机输入生成更进一步。您可以选择输入分布,这样您的软件的特定部分就可以比简单随机的更多地运用。

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

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

如果您在a范围内随机选择bint,则您有50%的时间FooBar 25%的时间Foobar 25%的时间。您可能会在Foo中找到比BarFoobar更多的错误。

如果您选择a使其在66.66%的时间内为负值,则BarFoobar的行使次数超过您的第一次分发。实际上,这三个分支每33.33%的时间被运用。

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

答案 6 :(得分:1)

我建议看看机械师:

  

<强> http://github.com/notahat/machinist/tree/master

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

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

答案 7 :(得分:0)

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

答案 8 :(得分:0)

您可能还会看到此主题:Testing with random inputs best practices

答案 9 :(得分:0)

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

如果RNG从不产生导致代码进入某种边缘情况的值,则不会覆盖此案例。如果您将RNG输出转换为您测试的代码输入的代码有缺陷,即使使用良好的生成器,您仍然可能无法完成所有边缘情况。

你将如何测试?

答案 10 :(得分:0)

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

测试背后的想法(特别是回归测试)是检查没有任何损坏。

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

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

通过存储生成的数据,您可以找到对此数据的“正确”响应。

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

答案 11 :(得分:0)

使用随机测试数据是一种很好的做法 - 硬编码的测试数据仅测试您明确考虑过的情况,而随机数据则会清除您可能错误的隐含假设。

我强烈推荐使用Factory Girl和ffaker。 (在任何情况下都不要使用Rails装置。)