如何进行不确定性的单元测试?

时间:2009-01-14 13:46:18

标签: unit-testing math statistics

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

[0.1, 0.1321, 0.0921, 0.012, 0.4]

这与全球最小值非常接近,所以这没关系。我们的第一种方法是选择一个阈值,如果结果发生得太高,让单元测试失败。不幸的是,这根本不起作用:结果似乎有一个高斯分布,因此,虽然不太可能,但即使算法仍然很好而且我们运气不好,测试也会不时发生。

那么,我该如何正确测试呢?我想这里需要相当多的统计数据。同样重要的是测试仍然很快,只需让测试运行几百次,然后取平均值就会太慢。

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

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

  • 不幸的是我无法为随机数生成器选择固定种子,因为我不想测试算法是否产生与以前完全相同的结果,但我想测试类似“有90%确定性我获得0.1或更高的结果“。

7 个答案:

答案 0 :(得分:15)

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

  1. 测试算法的整体有效性
  2. 测试算法实施的完整性
  3. 由于算法涉及随机化,(1)难以进行单元测试。任何随机过程的测试都会在某些时间内失败。您需要了解一些统计信息才能了解它应该失败的频率。有很多方法可以在测试的严格程度和失败的频率之间进行权衡。

    但是有一些方法可以为(2)编写单元测试。例如,您可以在运行单元测试之前将种子重置为特定值。然后输出是确定性的。这不会让你评估算法的平均有效性,但那是(1)。这样的测试可以作为一个旅程线:如果有人在维护期间将错误引入代码中,确定性单元测试可能会捕获该错误。

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

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

答案 1 :(得分:7)

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

我会采用5种优化算法中的每一种并测试它们以确保给定一组输入x,每次都会得到y的优化值。

编辑:要处理系统的随机组件,您可以引入传递种子以供使用的随机数生成器,也可以使用模拟库(ala RhinoMocks)当RNG被要求提供随机数时,强制它使用特定的数字。

答案 2 :(得分:7)

您的算法可能有一个随机组件。把它控制住。

你可以

  1. 允许调用者为随机数生成器选择种子。然后在测试中使用硬编码的种子。
  2. 让呼叫者提供随机数生成器。然后在测试中使用伪随机数生成器。
  3. 第二个选项可能是最好的,因为这样可以更容易地推断出算法的正确结果。

    当单元测试算法时,您要验证的是您已正确实施该算法。不是算法是否完成它应该做的事情。单元测试不应将待测代码视为黑盒子。

    您可能希望有一个单独的“性能”测试来比较不同算法的执行方式(以及它们是否真正有效),但您的单元测试实际上是用于测试算法的实现

    例如,在实现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);
    

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

答案 3 :(得分:5)

让测试运行,如果其中任何一个失败,重新运行这些测试 50次并查看它们失败的时间比例。 (当然是以自动方式。)

答案 4 :(得分:1)

我建议,不要让你的测试针对产生高斯分布的代码运行,而是创建一个蒙特卡洛类型算法,该算法多次运行该方法,然后测试结果的总体分布使用适当的分布模型。例如,如果它是平均值,则 将能够针对确定的阈值进行测试。如果它更复杂,您需要创建对适当分布进行建模的代码(例如,值&lt; x构成我结果的y%)。

请记住,您没有测试数字生成器,您正在测试生成值的单位!

答案 5 :(得分:1)

感谢所有答案,我现在正在这样做:

  1. 运行测试5次并取中位数结果。
  2. 如果中位数结果低于某个阈值,则测试成功。
  3. 如果阈值失败,再次测试直到达到阈值(测试成功)或直到我完成了这么多次迭代(大约100次左右),我可以非常肯定中位数不会低于阈值任何更多。
  4. 这样一旦测试看起来会失败,就会经常重新计算,直到确定它确实失败为止。

    这似乎有效,但我不太满意,因为我只测试中位数结果。

答案 6 :(得分:0)

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

由于结果的随机性,您可能需要对算法的各个部分进行单元测试,以确保它确实能够实现预期的效果。