代码之家  ›  专栏  ›  技术社区  ›  martinus

如何在不确定的情况下进行单元测试?

  •  16
  • martinus  · 技术社区  · 16 年前

    我们有几个不同的优化算法,它们为每次运行产生不同的结果。例如,优化的目标可以是找到一个函数的最小值,其中0是全局最小值。优化运行返回如下数据:

    [0.1, 0.1321, 0.0921, 0.012, 0.4]
    

    这非常接近全球最小值,所以这是可以的。我们的第一个方法是选择一个阈值,如果结果太高,那么让单元测试失败。不幸的是,这根本不起作用:结果似乎具有高斯分布,因此,尽管不太可能,但有时测试失败,即使算法仍然良好,我们只是运气不好。

    那么,我怎样才能正确地测试这个呢?我想这里需要一些统计数据。同样重要的是,测试速度仍然很快,只需让测试运行100次,然后取平均值就太慢了。

    以下是一些进一步的澄清:

    • 例如,我有一个算法可以将一个圆拟合成一组点。它速度极快,但并不总是产生相同的结果。我想编写一个单元测试来保证在大多数情况下它是足够好的。

    • 不幸的是,我不能为随机数生成器选择一个固定种子,因为我不想测试算法是否产生与以前完全相同的结果,但我想测试“90%确定度,我得到的结果是0.1或更好”。

    7 回复  |  直到 14 年前
        1
  •  15
  •   John D. Cook    14 年前

    听起来您的优化器需要两种测试:

    1. 测试算法的整体有效性
    2. 测试算法实现的完整性

    由于算法涉及随机化,(1)很难进行单元测试。任何对随机过程的测试都将在一定比例的时间内失败。你需要知道一些统计数据来了解它应该多久失败一次。有一些方法可以权衡你的测试有多严格和测试失败的频率。

    但是有一些方法可以为(2)编写单元测试。例如,可以在运行单元测试之前将种子重置为特定值。那么输出是确定的。这不允许您评估算法的平均有效性,但这是为了(1)。这样的测试可以充当一个绊脚石:如果有人在维护期间在代码中引入了一个bug,确定性单元测试可能会捕获这个bug。

    可能还有其他东西可以进行单元测试。例如,无论随机化部分发生了什么,您的算法都有可能返回一定范围内的值。也许某个值应该总是正的,等等。

    更新 我在《美丽的测试》一书中写了一章关于这个问题。见第10章: Testing a Random Number Generator .

        2
  •  7
  •   Matthew Brubaker    16 年前

    单元测试不应具有未知的通过/失败状态。如果您的算法在多次使用相同的输入运行时返回不同的值,那么您可能在算法中做了一些错误的事情。

    我将对5个优化算法中的每一个进行测试,以确保给定一组输入x,每次都能得到y的优化值。

    编辑 :要处理系统的随机组件,您可以引入将要使用的随机数生成器的种子传递功能,或者可以使用模拟库(Ala Rhinomocks)在RNG请求随机数时强制它使用特定的数字。

        3
  •  7
  •   Rasmus Faber    16 年前

    您的算法可能有一个随机分量。控制住它。

    你也可以

    1. 允许调用方为随机数生成器选择种子。然后在测试中使用硬编码种子。
    2. 让调用者提供一个随机数生成器。然后在测试中使用假随机数生成器。

    第二个选项可能是最好的,因为这将使您更容易理解算法的正确结果。

    当单元测试算法时,您想要验证的是您已经正确地实现了算法。不是算法是否做了它应该做的。单元测试不应将测试中的代码视为黑盒。

    您可能希望有一个单独的“性能”测试来比较不同算法的性能(以及它们是否实际工作),但是您的单元测试实际上是为了测试 实施 算法的。

    例如,在实现foo bar baz优化算法(tm)时,您可能意外地编写了x:=x/2而不是x:=x/3。这可能意味着该算法的工作速度较慢,但仍然可以找到相同的算法。您将需要白盒测试来发现这样的错误。

    编辑:

    不幸的是,我不能为随机数生成器选择一个固定种子,因为我不想测试算法是否产生与以前完全相同的结果,但我想测试“90%确定度,我得到的结果是0.1或更好”。

    我看不出任何方法来做一个既自动可验证又随机的测试。尤其是如果你想有机会区分实际误差和统计噪声。

    如果你想测试“90%的确定度,我得到0.1或更好的结果”,我建议如下:

    double expectedResult = ...;
    double resultMargin = 0.1;
    int successes = 0;
    for(int i=0;i<100;i++){
      int randomSeed = i;
      double result = optimizer.Optimize(randomSeed);
      if(Math.Abs(result, expectedResult)<resultMargin)
        successes++; 
    }
    Assert.GreaterThan(90, successes);
    

    (请注意,此测试具有确定性)。

        4
  •  5
  •   Jon Skeet    16 年前

    让测试运行,如果其中任何一个失败,则重新运行 只是那些测试 50次,看看他们失败的时间比例。(当然是自动的。)

        5
  •  1
  •   Mark Brittingham    16 年前

    我建议你不要 测试 针对产生高斯分布的代码运行,您创建了一个蒙特卡罗类型的算法,该算法多次运行该方法,然后 测试结果的总体分布 使用适当的分布模型。例如,如果它是一个平均值,那么 能够根据一个固定的阈值进行测试。如果更复杂,您需要创建代码来为适当的分布建模(例如,do values<x占我结果的y%)。

    请记住,您不是在测试数字生成器,而是在测试生成值的单元!

        6
  •  1
  •   martinus    16 年前

    谢谢你的回答,我现在要做的是:

    1. 运行测试5次,并取中间结果。
    2. 如果中位数结果低于某个阈值,则测试成功。
    3. 如果阈值失败,请再次测试,直到达到阈值(测试成功),或者直到我进行了如此多的迭代(大约100次左右),我可以非常确定中值不再低于阈值。

    这样,每当一个测试看起来要失败时,它就会经常重新计算,直到确定它确实失败为止。

    这似乎可行,但我并不十分满意,因为我只是在测试中值结果。

        7
  •  0
  •   Spoike Otávio Décio    16 年前

    junit和nunit都可以用公差/delta值断言浮点数据类型。也就是说,你测试输出值是否正确,给或取小数。在您的情况下,您要检查的正确值是0,如果您希望通过给定输出中的所有值,则公差为0.5(或0.20,公差为+/-0.20)。

    由于结果的随机性,您可能希望对算法的各个部分进行单元测试,以确保它真正做到了预期的效果。