【问题标题】:JUnit Test method with randomized nature具有随机性的 JUnit 测试方法
【发布时间】:2011-02-25 16:36:59
【问题描述】:

我目前正在为自己做一个小项目,并以此为契机熟悉单元测试并维护适当的文档。

我有一个 Deck 类,它代表一副纸牌(它非常简单,老实说,我可以确定它在没有单元测试的情况下也能工作,但就像我说的那样,我已经习惯了使用单元测试),它有一个shuffle() 方法,可以改变牌组中卡片的顺序。

实现非常简单,肯定会奏效:

public void shuffle()
{
    Collections.shuffle(this.cards);
}

但是,我怎样才能为这种方法实现单元测试。我的第一个想法是在调用shuffle() 后检查套牌的顶牌是否不同,但当然也有可能是相同的。我的第二个想法是检查卡片的整个顺序是否发生了变化,但它们可能再次处于相同的顺序。那么,我如何编写一个测试来确保该方法在所有情况下都有效?而且,一般来说,您如何对结果取决于某种随机性的方法进行单元测试?

干杯,

皮特

【问题讨论】:

  • 从逻辑上讲,如果顺序保持不变,你的牌会被认为是洗牌吗?
  • 关于您的最后一个问题,请参阅stackoverflow.com/questions/122741/…。我也认为 D. Knuth 有一个完整的章节。
  • 我认为如果牌保持相同的顺序并且发生了一些重新排序,则认为牌组被洗牌是有意义的,尽管不太可能洗牌真正的牌组回到原来的顺序。
  • 如果您的代码使用随机数生成器,请在单元测试中将其种子设置为固定值。这样你的套牌在你的测试中总是会以同样的方式洗牌。这可能需要更改 API 以使您的代码能够公开公开设置 rand 种子的方法,该方法可以在 JUnit 测试中设置

标签: java unit-testing junit random


【解决方案1】:

断言你的洗牌方法是否真的洗牌是非常困难的,如果不是不可能的话。默认随机数生成器仅在一定程度上是随机的。无法测试您是否对这种程度的随机性感到满意,因为这会花费太多时间。您实际测试的是随机数生成器本身,它没有多大意义。

但是,您可以测试的是此方法的invariants

  • 如果您退出该方法,则牌组中的牌数应该与您进入时完全相同。
  • shuffle 方法不应引入重复。

您当然可以创建一个测试,检查在n 洗牌序列中是否没有返回重复的牌组。但是偶尔这个测试可能会失败(但不太可能,正如其他答案中已经说明的那样)。

另外需要考虑的是随机数生成器本身。如果这只是一个玩具项目,java.util.Random 就足够了。如果您打算创建一些在线纸牌游戏,请考虑使用java.security.SecureRandom

【讨论】:

    【解决方案2】:

    首先,让我们考虑一下所涉及的概率:

    1. 您不能保证洗牌不会将卡片按确切顺序放置。但是,使用 52 张牌的套牌进行此操作的概率是 1 / 52! (即它是最小的,可能不值得担心。)

    2. 你肯定需要检查整副牌,因为顶牌与洗牌前相同的概率是 1 / 52。

    对于一般情况,假设您使用的是java.util.Random 数字生成器,只需使用相同的种子对其进行初始化。那么预先确定的输入的输出应该是可重复的。

    但是,特别是对于这种情况,假设您还没有实现自己的 List 我真的不明白测试 Collections.shuffle(List<?> list)Collections.shuffle(List<?> list, Random rnd) (API link) 的意义,因为这些只是Java API。

    【讨论】:

      【解决方案3】:

      另一种方法是使用shuffle(List<?> list, Random random) 方法并注入一个以常量为种子的Random 实例。

      这样您的 JUnit 测试可以运行一系列调用并检查输出是否为预期输出。

      您的类的正常实现会创建一个未播种的Random 实例。

      【讨论】:

        【解决方案4】:

        实际上,您将所有艰苦的工作都委派给了java.util.Collections 班级。这是 Java 集合 API 中的一个中心类,您应该假设它的工作方式与您可能对 java.lang.String 类所做的一样。

        我宁愿建议针对接口进行编码,并使用shuffle() 方法模拟/存根您的实现类。然后,您可以断言您对 shuffle() 方法的调用实际上是从您的测试中调用的,而不是与 Sun/Oracle 人员之前彻底测试过的完全一样的测试。

        这使您能够更专注于测试您自己的代码,其中 99.9% 的错误可能都在其中。例如,如果您将 java.util.Collections.shuffle() 方法替换为来自另一个框架或您自己的实现的方法,您的集成测试仍然可以工作!

        我知道您这样做是因为您想学习,并且我相信有关从其他框架中存根/模拟逻辑的知识作为您的测试知识的一部分非常有用。

        【讨论】:

          【解决方案5】:

          我想你的套牌里有 52 张牌。在随后的两次调用中获得相同订单的可能性非常低,所以我不会太在意。但是,如果您确实开始多次获得类似的套牌,我认为可以肯定地说您的随机数生成器存在一些问题。

          所以,答案是:检查整个牌组的顺序是否不同。

          另外,我认为你可以安全地要求你的 shuffle() 方法不要连续两次以相同的顺序返回卡片。如果您想绝对确保遵循该要求,您可以检查方法实现中的相似性。

          【讨论】:

          • 还要检查每副牌所代表的牌组是否相同(即,相同数量的牌,一旦忽略顺序,相同的成员)。
          【解决方案6】:

          有趣的问题。在我看来,最好的方法是将每个“洗牌”存储在一个集合中,然后在每次洗牌后比较你的套牌是否与该系列中的任何先前“套牌”匹配。

          根据您需要的“随机性”数量,您将增加存储在该单元测试中的洗牌套牌数量,即在 50 次洗牌后,您将拥有 50 个“套牌”的集合

          【讨论】:

            【解决方案7】:

            大多数人似乎认为您应该测试您正在测试的内容。我的意思是你正在构建的东西(或集成,当你确保第三方库确实按照它所说的那样做时)。

            但您不应该测试 Java 语言本身。

            应该有一些测试原则,例如“不要测试 PlusEquals”。

            【讨论】:

              【解决方案8】:

              我曾在建模和模拟框架中处理随机数,并遇到过一个类似的问题:我如何才能对我们的 PRNG 实现进行实际单元测试。最后我真的没有做。相反,我所做的是执行一些健全性检查。例如,我们的 PRNG 都宣传它们生成了多少位,所以我检查了这些位是否确实发生了变化(大约 10k 次迭代),而所有其他位都为 0。我检查了有关种子的正确行为(使用相同的初始化 PRNG种子必须产生相同的数字序列)等。然后我决定将实际的随机性测试放入交互式 UI 中,以便可以在需要时对其进行测试,但对于单元测试,非确定性结果并不是那么好,我想。

              【讨论】:

                【解决方案9】:

                您可以反复洗牌,记录黑桃 A(或其他牌,或所有其他牌)有多少次成为套牌中的第一张牌。理论上,卡片应该在 52 次洗牌中的 1 次出现在顶部。收集完所有数据后,将实际频率与数字 1/52 进行比较,并检查差值(绝对值)是否低于某个选定的 epsilon 值。您洗牌的次数越多,您的 epsilon 值就越小。如果您的 shuffle() 方法将卡片置于您的 epsilon 阈值之内,那么您可以确定它会按照您的意愿随机化卡片。

                而且您不必只停留在最上面的牌上。您可以测试卡组中的每个位置是否给出相同的结果。用一张卡做,做所有的卡,这可能没关系。这可能有点矫枉过正,但它可以保证你的 shuffle() 工作正常。

                【讨论】:

                  猜你喜欢
                  • 2019-06-22
                  • 2011-08-13
                  • 1970-01-01
                  • 2014-01-07
                  • 2015-09-27
                  • 1970-01-01
                  • 2012-02-24
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多