【问题标题】:Is it a bad practice to randomly-generate test data?随机生成测试数据是一种不好的做法吗?
【发布时间】:2009-03-11 21:02:10
【问题描述】:

自从我开始使用 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。粗心的程序员可能会通过在before(:all) 中创建Item 或通过将另一个夹具插入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 中的非确定性故障会提示我出了点问题。如果我只使用fixture,可能需要更长的时间才能发现这样的错误。

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

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

【问题讨论】:

  • 行“3.times { @items 50})”看起来不对。
  • 而现在,仅仅 58 个月后,我回应...它看起来不正确,因为它有“<<”在里面......但没有正确逃脱。

标签: ruby testing rspec fixtures


【解决方案1】:

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

【讨论】:

  • +1 表示可重现的评论。控制随机发生器的初始状态非常重要。如果你发现一个奇怪的行为,你会想再试一次。
  • 对可重复性的另一个 +1。
  • 第三。当我使用随机测试时,我总是添加一种报告和设置种子的方法。虽然总的来说我尽量避免依赖随机化......
  • 我会记录生成的种子,而不是拥有一个恒定的种子。
【解决方案2】:

这是对你第二点的回答:

(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

【讨论】:

  • FixtureReplacement 链接是否损坏?
  • 很多优秀的答案,但这一点切入正题——有更好的方法来做我想做的事,而且我的测试数据不必再是“随机的”了。
  • bobocopy:看起来是这样。奇怪,我认为它昨天工作。现在已经修好了。
【解决方案3】:

在我最近的一个项目中,我们对此进行了很多思考。最后,我们确定了两点:

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

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

【讨论】:

  • 我广泛使用随机测试数据。我从来没有遇到过麻烦大于价值的情况。我的随机测试很简单,我总能准确说出它们失败的原因。我进行了随机测试,发现我的代码中有错误的假设。随机测试用例比硬编码测试用例好得多,应尽可能使用。如果可以避免的话,永远不要对测试数据进行硬编码——那是在纸牌游戏中作弊。
  • 另外,您不需要可重复的随机数生成器。失败的测试用例中的值转储也同样有效。
【解决方案4】:

已经发布了很多很好的信息,但另请参阅:Fuzz Testing。众所周知,微软在他们的很多项目中都使用了这种方法。

【讨论】:

  • 我很高兴有人提出这个问题。模糊测试非常有用,但请注意,随机测试应该是除了可重复测试之外。
  • @vasi 如果“random”包含伪随机,那么它不会与可重复性冲突。记录种子怎么样?
【解决方案5】:

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

【讨论】:

    【解决方案6】:

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

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

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

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

    如果您在int 范围内随机选择ab,则您有50% 的时间锻炼FooBar 25% 的时间和Foobar 25% 的时间锻炼。您可能会在Foo 中发现比在BarFoobar 中更多的错误。

    如果您选择 a 使得 66.66% 的时间为负数,则 BarFoobar 会比您的第一次分配得到更多的锻炼。事实上,三个分支各有 33.33% 的时间得到锻炼。

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

    【讨论】:

    • 您不需要对此进行统计测试 - 只需在输入和输出之间建立可测量的关系。
    【解决方案7】:

    我建议看看机械师:

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

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

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

    【讨论】:

    • 是否需要 ActiveRecord/Rails 才能使用机械师?
    • 我相信它确实依赖于 ActiveRecord,但你可以在 Rails 之外使用它。
    【解决方案8】:

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

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

    【讨论】:

      【解决方案9】:

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

      【讨论】:

      • 测试和代码相互测试。如果您的测试有错误,您很快就会发现。 :)
      【解决方案10】:

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

      【讨论】:

        【解决方案11】:

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

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

        你将如何测试?

        【讨论】:

          【解决方案12】:

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

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

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

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

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

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2011-02-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2013-05-13
            • 2018-09-26
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多