固有随机/非确定性算法的单元测试


41

简而言之,我当前的项目涉及“约束随机事件”的创建。我基本上是在制定检查时间表。其中一些是基于严格的计划约束;您每周星期五10:00 AM进行一次检查。其他检查是“随机的”;有一些基本的可配置要求,例如“每周必须进行3次检查”,“必须在9 AM-9PM的时间之间进行检查”以及“在同一8小时内不应进行两次检查”,但是在为一组特定的检查配置的任何限制内,得出的日期和时间均不可预测。

单元测试和TDD,IMO在此系统中具有巨大的价值,因为它们可以用于按增量方式构建它,而整套需求仍然不完整,并确保我不会“过度设计”它来做我不喜欢的事情目前不知道我需要。严格的时间表对TDD来说是小菜一碟。但是,当我为系统的随机部分编写测试时,我发现很难真正定义要测试的内容。我可以断言调度程序产生的所有时间都必须在约束范围内,但是我可以实现通过所有此类测试的算法,而实际时间却不是很“随机”。实际上,这正是发生的事情。我发现了一个问题,尽管时间无法精确预测,但它属于允许的日期/时间范围的一小部分。该算法仍然通过了我认为我可以合理做出的所有断言,并且我无法设计在这种情况下会失败的自动测试,但是在给出“更​​多随机”结果时通过。我必须证明该问题是通过重组一些现有测试以重复多次来解决的,并目视检查生成的时间是否在整个允许范围内。

有没有人提供设计非预期行为的提示?


感谢所有的建议。主要观点似乎是,我需要进行确定性测试才能获得确定性,可重复性和可肯定的结果。说得通。

我创建了一组“沙盒”测试,其中包含用于约束过程(可能为任意长的字节数组在最小值和最大值之间变长的过程)的候选算法。然后,我通过一个FOR循环运行该代码,该循环为该算法提供了几个已知的字节数组(刚开始时从1到10,000,000的值),并使该算法将每个值限制为1009到7919之间的值(我使用质数来确保算法不会在输入和输出范围之间经过一些偶然的GCF)。计算得到的约束值,并生成直方图。要“通过”,所有输入都必须反映在直方图中(为了确保我们不会“丢失”任何值),直方图中任意两个存储桶之间的差不能大于2(实际上应小于等于1) ,但请继续关注)。获胜的算法(如果有)可以直接剪切并粘贴到生产代码中,并进行永久测试以进行回归。

这是代码:

    private void TestConstraintAlgorithm(int min, int max, Func<byte[], long, long, long> constraintAlgorithm)
    {
        var histogram = new int[max-min+1];
        for (int i = 1; i <= 10000000; i++)
        {
            //This is the stand-in for the PRNG; produces a known byte array
            var buffer = BitConverter.GetBytes((long)i);

            long result = constraintAlgorithm(buffer, min, max);

            histogram[result - min]++;
        }

        var minCount = -1;
        var maxCount = -1;
        var total = 0;
        for (int i = 0; i < histogram.Length; i++)
        {
            Console.WriteLine("{0}: {1}".FormatWith(i + min, histogram[i]));
            if (minCount == -1 || minCount > histogram[i])
                minCount = histogram[i];
            if (maxCount == -1 || maxCount < histogram[i])
                maxCount = histogram[i];
            total += histogram[i];
        }

        Assert.AreEqual(10000000, total);
        Assert.LessOrEqual(maxCount - minCount, 2);
    }

    [Test, Explicit("sandbox, does not test production code")]
    public void TestRandomizerDistributionMSBRejection()
    {
        TestConstraintAlgorithm(1009, 7919, ConstrainByMSBRejection);
    }

    private long ConstrainByMSBRejection(byte[] buffer, long min, long max)
    {
        //Strip the sign bit (if any) off the most significant byte, before converting to long
        buffer[buffer.Length-1] &= 0x7f;
        var orig = BitConverter.ToInt64(buffer, 0);
        var result = orig;
        //Apply a bitmask to the value, removing the MSB on each loop until it falls in the range.
        var mask = long.MaxValue;
        while (result > max - min)
        {
            mask >>= 1;
            result &= mask;
        }
        result += min;

        return result;
    }

    [Test, Explicit("sandbox, does not test production code")]
    public void TestRandomizerDistributionLSBRejection()
    {
        TestConstraintAlgorithm(1009, 7919, ConstrainByLSBRejection);
    }

    private long ConstrainByLSBRejection(byte[] buffer, long min, long max)
    {
        //Strip the sign bit (if any) off the most significant byte, before converting to long
        buffer[buffer.Length - 1] &= 0x7f;
        var orig = BitConverter.ToInt64(buffer, 0);
        var result = orig;

        //Bit-shift the number 1 place to the right until it falls within the range
        while (result > max - min)
            result >>= 1;

        result += min;
        return result;
    }

    [Test, Explicit("sandbox, does not test production code")]
    public void TestRandomizerDistributionModulus()
    {
        TestConstraintAlgorithm(1009, 7919, ConstrainByModulo);
    }

    private long ConstrainByModulo(byte[] buffer, long min, long max)
    {
        buffer[buffer.Length - 1] &= 0x7f;
        var result = BitConverter.ToInt64(buffer, 0);

        //Modulo divide the value by the range to produce a value that falls within it.
        result %= max - min + 1;

        result += min;
        return result;
    }

...结果如下:

在此处输入图片说明

LSB拒绝(将数字移位直到其在该范围内)是很麻烦的,原因很容易解释。当您将任何数字除以2直到小于最大值时,您会立即退出,并且对于任何非平凡的范围,都会使结果偏向上三分之一(如直方图的详细结果所示) )。这正是我从完成日期中看到的行为。所有时间都是在特定日子的下午。

MSB拒绝(将最高有效位一次从最高位删除直到它在范围内)会更好,但是又一次,因为您要砍掉每一位很大的数字,所以它的分布不均。您不太可能在高端和低端获得数字,因此您会偏向中间三分之一。这可能有益于希望将随机数据“规范化”为钟形曲线的人,但是两个或多个较小的随机数之和(类似于掷骰子)将为您提供更自然的曲线。就我而言,它失败了。

唯一通过此测试的方法是通过模除约束,这也是三者中最快的。根据定义,Modulo将根据可用输入产生尽可能均匀的分布。


2
因此,最终,您需要一个程序来查找随机数生成器的输出并确定它是否是随机的?如“ 5,4,10,31,120,390,2,3,4”是随机的,而“ 49,39,1,10,103,12,4,189”不是随机的吗?
psr 2012年

不可以,但最好避免在实际的PRNG和最终结果之间引入偏差。
KeithS 2012年

然后,听起来像是模拟PRNG就可以了。您不需要实际的随机值来测试您是否破坏了值。如果您有将随机值压缩到允许范围的子集太小的错误,那么您一定弄错了一些特定的值。
psr 2012年

您还应该测试组合。每小时具有大致相等的预期检查数并不能防止这种情况,例如,总是在星期二上午11点进行检查,然后在星期四下午2点进行检查,在星期五上午10点进行检查。
David Thornley,2012年

这更多地是对PRNG本身的考验。如上所述,对约束机制的测试总是会失败,因为它被提供了完全非随机的数据集。假设约束机制不努力对随机数据进行“排序”,我称之为“外部测试”,这是单元测试不应该做的事情。
KeithS

Answers:


17

我假设您实际上要在此处测试的是,给定随机化器的一组特定结果,您的其余方法将正确执行。

如果这正是您要寻找的内容,请模拟随机发生器,使其在测试领域内具有确定性。

我通常具有用于各种不确定性或不可预测(在编写测试时)数据的模拟对象,包括GUID生成器和DateTime.Now。

编辑,摘自评论:您必须在尽可能低的水平上模拟PRNG(昨晚我逃避了这个术语),即。当它生成字节数组时,而不是在将它们转换为Int64之后。或什至在两个级别上,您都可以测试到Int64数组的转换是否按预期进行,然后分别测试您到DateTimes数组的转换是否按预期进行。正如Jonathon所说,您可以通过给它设置种子来做到这一点,或者可以给它提供要返回的字节数组。

我更喜欢后者,因为如果PRNG的框架实现发生变化,它不会中断。但是,为它提供种子的一个好处是,如果您在生产中发现一个案例无法按预期工作,则只需记录一个数字即可复制它,而不是整个数组。

说了这么多,您必须记住,由于某种原因,它被称为随机数生成器。即使在该水平上也可能存在一些偏差。


1
否。在这种情况下,我要测试的是随机化器本身,并断言由随机化器生成的“随机”值落在指定的约束范围内,而仍然是“随机”,因为它不会偏向于允许范围内的不均匀分布时间范围。我可以确定地测试特定的日期/时间正确地通过或未通过特定的约束,但是我遇到的真正问题是随机化器产生的日期是有偏差的,因此是可预测的。
KeithS 2012年

我唯一能想到的就是让随机化器吐出一堆日期并创建一个直方图,然后断言值相对均匀地分布。这似乎是极端笨拙的,而且还不确定,因为任何真正随机的数据集都可能显示出明显的偏差,然后较大的一组数据就会反驳。
KeithS 2012年

1
该测试会偶尔且不可预测地中断。你不要那样 老实说,我认为您误解了我的观点。在您所说的随机数发生器内的某处,必须有一行代码生成一个随机数,不是吗?该行就是我所说的随机化器,而您称为随机化器的其余部分(基于“随机”数据的日期分布)就是您要测试的内容。还是我错过了什么?
pdr 2012年

仅当您做的样本很小时,随机序列的正确统计量度(相关性,块相关性,平均值,标准差等)才会无法满足您的期望范围。增加采样集和/或增加允许的误差线
lurscher 2012年

1
“即使在那个水平上也存在一些偏差”如果您使用良好的PRNG,那么除了真正的随机性之外,您将无法找到任何可以告诉它的测试(具有实际的计算范围)。因此,在实践中,可以假设一个好的PRNG毫无偏见。
CodesInChaos

23

这听起来像是一个愚蠢的答案,但是我要把它扔在那里,因为这是我以前看到的方法:

将代码与PRNG分离开来-将随机化种子传递到所有使用随机化的代码中。然后,您可以从单个种子(或多个种子)确定“工作”值,从而使您感觉更好。这将使您能够充分测试代码,而不必依赖大数定律。

听起来很疯狂,但这就是军方的做法(或者他们使用的“随机表”根本不是随机的)


确实:如果您无法测试算法的元素,请对其进行抽象并进行模拟
Steve Greatrex,2012年

我没有指定确定性的种子值。相反,我完全删除了“ random”元素,因此我什至不必依赖特定的PRNG算法。我能够进行测试,在给定范围较大的均匀分布的数字的情况下,我使用的算法可以将其限制在较小的范围内,而不会引入偏差。无论开发者是谁,PRNG本身都应该经过充分的测试(我使用的是RNGCryptoServiceProvider)。
KeithS 2012年

关于“随机表”方法,您还可以使用包含“可逆”数字生成算法的测试实现。这使您可以“倒带” PRNG,甚至查询它以查看最后N个输出是什么。在某些情况下,它将允许进行更深入的调试。
达连

这并不是那么愚蠢-根据他们的论文,这是Google在Spanner测试中重现故障注入的相同方法:)
Akshat Mahajan

6

“随机(足够)了吗”变成一个难以置信的微妙问题。简短的答案是,传统的单元测试不会削减它-您需要生成一堆随机值,并将其提交给各种统计测试,从而使您高度自信它们足够随机以满足您的需求。

会有一个模式-毕竟我们使用的是伪随机数生成器。但是在某些时候,事情对于您的应用程序来说将是“足够好的”(足够好的性能在一端的多个游戏之间变化很多,相对简单的生成器就足够了,一直到密码学,在这种情况下,您真正​​需要序列来确定是不可行的)由坚定而装备精良的攻击者)。

Wikipedia文章http://en.wikipedia.org/wiki/Randomness_tests及其后续链接提供了更多信息。


即使是平庸的PRNG在任何统计测试中也不会显示出任何模式。对于良好的PRNG,将它们与实际随机数区分开来几乎是不可能的。
CodesInChaos

4

我有两个答案给你。

===第一答案===

看到您的问题的标题后,我便开始提出解决方案。我的解决方案与其他几个提议的解决方案相同:模拟您的随机数生成器。毕竟,为了编写好的单元测试,我已经构建了几个需要此技巧的不同程序,并且我开始在所有编码中将对随机数的可模拟访问作为一种标准实践。

但后来我读了你的问题。对于您描述的特定问题,这不是答案。您的问题不是您需要使使用随机数的过程成为可预测的(因此它是可测试的)。而是,您的问题是要验证您的算法是否将RNG的均匀随机输出映射到算法的均匀约束输出中-如果基础RNG均匀,则会导致检查时间均匀分布(取决于问题约束)。

这是一个非常困难(但定义明确)的问题。这意味着这是一个有趣的问题。立即开始想到一些解决该问题的好主意。当我还是一个热门的程序员时,我可能已经开始用这些想法做些事了。但是我不再是一个炙手可热的程序员了……我喜欢我现在更有经验和技能。

因此,我没有沉迷于棘手的问题,而是对自己说:这有什么价值?答案令人失望。您的错误已得到解决,以后您将继续努力解决此问题。外部环境不能触发问题,只能对算法进行更改。解决此有趣问题的唯一原因是为了满足TDD(测试驱动设计)的实践。如果我了解到一件事,那就是在不有价值的情况下盲目遵循任何做法都会导致问题。我的建议是:不要为此编写任何测试,然后继续。


===第二答案===

哇...好酷的问题!

您需要做的是编写一个测试,以验证您选择检查日期和时间的算法是否会产生均匀分布(在问题约束内)的输出(如果使用的RNG产生均匀分布的数字)。这是几种方法,按难度级别排序。

  1. 您可以施加暴力。只需运行整个算法一整遍,并以实际的RNG作为输入。检查输出结果以查看它们是否均匀分布。如果分布从完全均匀变化到超过某个阈值,您的测试将需要失败,并且为了确保您发现问题,不能将阈值设置得太低。这意味着您将需要大量运行,以确保错误肯定(通过随机机会导致测试失败)的可能性非常小(对于中等大小的代码库,该错误率<1%;对于更少的代码库,此错误率甚至更低)很大的代码库)。

  2. 将您的算法视为将所有RNG输出的级联作为输入,然后产生检查时间作为输出的函数。如果您知道此函数是分段连续的,则有一种方法可以测试您的属性。用可模拟的RNG替换RNG并多次运行该算法,从而产生均匀分布的RNG输出。因此,如果您的代码需要2个RNG调用,每个调用的范围为[0..1],则您可能需要测试该算法运行100次,并返回值[(0.0,0.0),(0.0,0.1),(0.0, 0.2),...(0.0,0.9),(0.1,0.0),(0.1,0.1),...(0.9,0.9)]。然后,您可以检查100个运行的输出是否(近似)均匀分布在允许的范围内。

  3. 如果您确实需要以可靠的方式验证算法,并且无法对算法进行假设或运行大量次,那么您仍然可以解决问题,但是您可能需要对算法编程进行一些限制。以PyPy及其对象空间方法为例。您可以创建一个对象空间,而不是实际执行算法,而只是计算输出分布的形状(假设RNG输入是统一的)。当然,这需要您构建这样的工具,并且算法必须在PyPy或其他易于编译器进行大量修改并使用它来分析代码的工具中构建。


3

对于单元测试,将随机生成器替换为可生成涵盖所有极端情况的可预测结果的类。也就是说,请确保您的伪随机化器连续生成最低可能值和最高可能值,并且产生相同的结果。

您不希望您的单元测试忽略例如Random.nextInt(1000)返回0或999时发生的一次性错误。


3

您可能想看看Sevcikova等人:“随机系统的自动测试:一种基于统计的方法”(PDF)。

该方法已在UrbanSim仿真平台的各种测试用例中实现。


那是好东西。
KeithS 2012年

2

简单的直方图方法是很好的第一步,但不足以证明随机性。对于统一的PRNG,您还将(至少)生成一个二维散点图(其中x是先前的值,y是新的值)。该图也应该是均匀的。由于系统中存在故意的非线性,因此在您的情况下这很复杂。

我的方法是:

  1. 验证(或假设)源PRNG足够随机(使用标准统计量度)
  2. 验证不受约束的PRNG到日期时间的转换在输出空间上是足够随机的(这证明转换中没有偏差)。在这里,您简单的一阶均匀性测试就足够了。
  3. 验证约束条件是否足够均匀(对有效料仓进行简单的一阶均匀性测试)。

这些测试中的每一个都是统计性的,并且需要大量样本点,以高度的置信度避免误报和误报。

关于转换/约束算法的性质:

给定:生成伪随机值p的方法,其中0 <= p <= M

需要:在(可能不连续的)范围内输出y 0 <= y <= N <= M

算法:

  1. 计算r = floor(M / N),即适合输入范围的完整输出范围的数量。
  2. 计算p的最大可接受值:p_max = r * N
  3. 生成p的值,直到p_max找到小于或等于的值
  4. 计算 y = p / r
  5. 如果y是可接受的,则将其返回,否则重复步骤3

关键是丢弃不可接受的值,而不是折叠不均匀。

用伪代码:

# assume prng generates non-negative values
def randomInRange(min, max, prng):
    range = max - min
    factor = prng.max / range

    do:
        value = prng()
    while value > range * factor
    return (value / factor) + min

def constrainedRandom(constraint, prng):
    do:
        value = randomInRange(constraint.min, constraint.max, prng)
    while not constraint.is_acceptable(value)

1

除了验证您的代码没有失败或在正确的地方抛出正确的异常之外,您还可以创建有效的输入/响应对(甚至手动计算),将输入提供给测试并确保其返回预期的响应。不太好,但是恕我直言,这几乎是您所能做的。但是,对于您而言,这并不是真正随机的,一旦创建了时间表,您就可以测试规则的符合性-每周必须进行9到9次之间的3次检查;进行检查时,没有真正的需求或没有能力测试确切的时间。


1

确实没有比运行多次并查看是否获得所需发行版更好的方法了。如果您有50个允许的潜在检查计划,则需要运行500次测试,并确保每个计划使用了接近10次。您可以控制随机生成器种子以使其更具确定性,但这也将使您的测试与实现细节更加紧密地耦合。


但是,如果它确实是随机的,那么有时根本就不会使用某些时间表。有时,某些时间表将使用20次以上。我不知道您打算如何测试每个进度表“接近10次”,但是无论您在此处进行什么条件测试,都会有一个测试,该测试有时会在程序正常工作时失败。
达伍德说恢复莫妮卡

@DawoodibnKareem具有足够的样本量(以及对均匀性的合理限制),可以将测试失败的可能性降低到十亿分之一。通常,这样的统计量与n呈指数关系,因此花费的时间比您预期的要少。
mbrig

1

无法测试没有具体定义的模糊状态。如果生成的日期通过所有测试,那么理论上您的应用程序将正常运行。计算机无法告知您日期是否足够“随机”,因为它无法确认进行此类测试的条件。如果所有测试都通过了,但是应用程序的行为仍然不适合,则从经验上讲,您的测试覆盖范围不足(从TDD角度来看)。

我认为,最好的办法是实施一些任意的日期生成约束,以便该分布通过人类气味测试。


2
您可以通过自动测试绝对确定随机性。您只需生成足够数量的样本并应用随机性标准测试即可检测系统中的偏差。这是一个相当标准的本科编程活动。
Frank Szczerba '02

0

只需记录随机化器的输出(伪或量子/混沌或现实世界)即可。然后,在构建单元测试用例时,保存并重播那些符合您的测试要求或暴露出潜在问题和错误的“随机”序列。


0

这种情况对于基于属性的测试似乎是理想的。

简而言之,它是一种测试模式,其中测试框架为被测代码生成输入,而测试断言验证输出的属性。然后,该框架可以足够聪明,以“攻击”被测代码,并尝试将其陷入错误。该框架通常也足够聪明,可以劫持您的随机数生成器种子。通常,您可以将框架配置为最多生成N个测试用例或最多运行N秒,并记住上次运行失败的测试用例,然后首先针对较新的代码版本重新运行这些测试用例。这允许在开发期间进行快速迭代,并在带外/ CI中进行缓慢,全面的测试。

这是一个测试sum功能的(哑巴,失败)示例:

@given(lists(floats()))
def test_sum(alist):
    result = sum(alist)
    assert isinstance(result, float)
    assert result > 0
  • 测试框架一次生成一个列表
  • 列表的内容将是浮点数
  • sum 被调用并验证结果的属性
  • 结果总是浮动的
  • 结果是肯定的

此测试将发现一堆“ bug” sum(如果您能够自己猜出所有这些,请注释):

  • sum([]) is 0 (int,不是浮点数)
  • sum([-0.9]) 是负面的
  • sum([0.0]) 不是严格肯定的
  • sum([..., nan]) is nan 这不是积极的

使用默认设置,hpythesis在找到1个“错误”输入后中止测试,这对于TDD是有利的。我认为可以将其配置为报告许多/所有“不良”输入,但是现在找不到该选项。

在OP情况下,经过验证的属性将更加复杂:存在检查类型A,每周检查类型A三次,检查时间B总是在12pm,检查类型C从9到9,[给定的时间表是一周]类型检查A,B,C全部存在,等等。

最著名的库是QuickCheck for Haskell,有关其他语言的库列表,请参见下面的Wikipedia页面:

https://en.wikipedia.org/wiki/QuickCheck

假设(适用于Python)对这种测试有很好的记录:

https://hypothesis.works/articles/what-is-property-based-testing/


-1

可以进行单元测试的逻辑是确定随机日期是否有效或是否需要选择另一个随机日期。

缺少一堆日期并确定它们是否适当地随机,就无法测试随机日期生成器。


-1

您的目标不是编写单元测试并通过它们,而是确保您的程序符合其要求。您这样做的唯一方法是首先精确定义您的需求。例如,您提到“每周随机检查3次”。我要说的要求是:(a)3次检查(不是2或4次),(b)那些不想被意外检查的人无法预测的时间,以及(c)并不太紧密-相隔五分钟的两次检查可能毫无意义,也可能相隔不远。

因此,您比我更准确地写下了要求。(a)和(c)很容易。对于(b),您可能会编写一些尽可能聪明的代码来尝试预测下一次检查,并通过单元测试,该代码必须不能比单纯的猜测更好地进行预测。

当然,您需要意识到,如果您的检查确实是随机的,那么任何预测算法都可能是偶然的,因此是正确的,因此,您必须确保您和您的单元测试不会发生恐慌。也许还要再做一些测试。我不会费心测试随机数生成器,因为最后才是重要的检查计划,而创建方式并不重要。


不就是不。单元测试证明该程序符合其要求,因此两者是相同的。而且我不从事编写对反向算法进行逆向工程的预测软件的业务。如果我不想告诉你这件事,那么我将通过预测它们的密钥并将机密卖给出价最高的人,来杀死可破解的安全网站。我的业务是编写一个调度程序,该调度程序创建的时间是可约束的,但在约束内是不可预测的,并且我需要确定性测试来证明自己已经做到了,而不是概率性的说我很确定的时间。
KeithS 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.