单元测试中的随机数据?

我有一个同事,他为随机数据填充字段的对象编写单元测试。他的理由是,它提供了更广泛的测试范围,因为它将测试许多不同的值,而正常的测试只使用一个静态值。

我给了他很多反对的理由,主要是:

  • 随机值意味着测试不是真正可重复的(这也意味着如果测试可以随机失败,它可以在构建服务器上这样做,并中断构建)
  • 如果它是一个随机值,测试失败,我们需要 a)修复对象和 b)强迫自己每次测试该值,所以我们知道它的工作,但因为它是随机的,我们不知道值是什么

另一位同事补充道:

  • 如果我正在测试一个异常,随机值将不能确保测试最终处于预期的状态
  • 随机数据用于刷新系统和负载测试,而不是用于单元测试

还有谁能给我更多的理由让他停止这么做吗?

(或者,这是一种可以接受的编写单元测试的方法,而我和我的其他同事都错了?)

58173 次浏览

根据您的对象/应用程序,随机数据将在负载测试中占有一席之地。我认为更重要的是使用显式测试数据边界条件的数据。

您可以生成一次随机数据(我的意思是只生成一次,而不是每次测试运行一次) ,然后在以后的所有测试中使用它吗?

我可以肯定地看到创建随机数据来测试那些您没有想到的情况的价值,但是您是对的,拥有可以随机通过或失败的单元测试是一件坏事。

  • 如果它是一个随机值,测试失败,我们需要 a)修复对象和 b)强迫自己每次测试该值,所以我们知道它的工作,但因为它是随机的,我们不知道值是什么

如果您的测试用例没有准确地记录它正在测试的内容,也许您需要重新编码测试用例。我总是希望有日志,我可以回头参考测试用例,以便我确切地知道是什么导致它失败,无论是使用静态数据还是随机数据。

这里有一个中途之家,有一些用途,这是种子你的 PRNG 与一个常数。这允许您生成可重复的“随机”数据。

就我个人而言,我确实认为有些地方(恒定的)随机数据在测试中是有用的——当你认为你已经完成了所有仔细考虑过的角落,使用来自 PRNG 的刺激有时可以找到其他东西。

在《 美丽的密码》一书中,有一章叫做“漂亮的测试”,在这一章中,他为 二进制搜索算法经历了一个测试策略。其中一段叫做“随机测试行为”,他创建随机数组来彻底测试算法。你可以在谷歌图书 第95页上在线阅读这些书籍,但这是一本值得拥有的好书。

所以基本上这只是表明,生成随机数据进行测试是一个可行的选择。

有个折衷方案。你的同事确实有所发现但我觉得他做错了。我不确定完全随机测试是否有用,但它肯定不是无效的。

程序(或单元)规范是一种假设,即存在一些满足它的程序。这个程序本身就是这个假设的证据。单元测试应该是提供反证据来反驳程序按照规范工作的尝试。

现在,您可以手工编写单元测试,但它实际上是一项机械任务。它可以自动化。您所要做的就是编写规范,然后机器就可以生成大量试图破坏代码的单元测试。

我不知道你用的是什么语言,但是看这里:

爪哇咖啡 Http://functionaljava.org/

Scala (或 Java) Http://github.com/rickynils/scalacheck

Haskell Http://www.cs.chalmers.se/~rjmh/quickcheck/

.NET: Http://blogs.msdn.com/dsyme/archive/2008/08/09/fscheck-0-2.aspx

这些工具将采用格式良好的规范作为输入,并自动生成任意数量的单元测试,以及自动生成的数据。他们使用“收缩”策略(您可以对其进行调整)来寻找最简单的可能测试用例来破坏代码,并确保它能够很好地覆盖边缘用例。

测试愉快!

你的人怎么能再次运行测试,当它已经失败,看看他是否已经修复它?也就是说,他失去了测试的重复性。

虽然我认为在测试中随机抛出大量数据可能有一些价值,正如在其他答复中提到的那样,它更多地属于负载测试的范畴。这基本上是一种“希望测试”的做法。我认为,在现实中,你的人只是没有想到他正在试图测试的东西,并通过希望随机性最终会捕捉到一些神秘的错误来弥补这种想法的缺乏。

因此,我对他的看法是,他在偷懒。或者,换句话说,如果他不花时间去理解他想要测试什么,这可能表明他并不真正理解他正在编写的代码。

我们今天才碰到这个。我想要 伪随机(这样它看起来就像压缩的音频数据在大小方面)。我做到了,我也想要 确定性。Rand ()在 OSX 和 Linux 上是不同的。除非我重新播种,否则它随时都可能改变。因此,我们将其改为确定性的,但仍然是假设随机的: 测试是可重复的,就像使用罐装数据一样(但更方便地编写)。

这是 没有测试通过一些随机蛮力通过代码路径。这就是区别: 仍然是确定性的,仍然是可重复的,仍然使用看起来像真实输入的数据来对复杂逻辑中的边缘情况运行一组有趣的检查。还是单元测试。

那还算是随机吗? 我们边喝啤酒边谈吧。 : -)

这种测试被称为 猴子测试。如果处理得当,它可以从真正黑暗的角落里把虫子熏出来。

为了解决您对重复性的担忧: 正确的方法是记录失败的测试条目,生成一个单元测试,探测特定 bug 的 全家人; 并且在单元测试中包含一个特定的输入(来自随机数据) ,这个输入导致了最初的失败。

如果您使用随机输入进行测试,则需要记录输入,以便查看值是什么。这样,如果遇到一些边界情况,您就可以编写 可以来重现它。我从人们那里听到过不使用随机输入的相同理由,但是一旦你了解了特定测试运行所使用的实际值,那么这就不是什么大问题了。

“任意”数据的概念作为表示 没有重要内容的一种方式也非常有用。我们有一些验收测试的想法,有很多噪音数据,是没有相关性的测试在手。

你的同事正在做 模糊测试,尽管他并不知道。他们在服务器系统中特别有价值。

对于查看测试的人来说,一个优势是任意数据显然不重要。我见过太多涉及数十个数据片段的测试,很难分辨哪些需要这样做,哪些恰好是这样做的。例如。如果一个地址验证方法是测试与特定的邮政编码和所有其他数据是随机的,那么您可以非常肯定的邮政编码是唯一重要的部分。

我可以设想三种解决测试数据问题的方法:

  • 使用固定数据进行测试
  • 使用随机数据进行测试
  • 生成随机数据 一次,然后使用它作为您的固定数据

我建议做 以上皆有。也就是说,编写可重复的单元测试,其中包括一些使用大脑编写的边缘案例,以及一些只生成一次的随机数据。然后编写一组运行 我也是的随机测试。

不要期望随机测试捕捉到你的可重复测试遗漏的东西。你的目标应该是用可重复的测试覆盖所有的内容,并且把随机测试看作是额外的奖励。如果他们发现了什么,那应该是你无法合理预测的东西,一个真正的怪人。

如果您正在进行 TDD,那么我认为随机数据是一种很好的方法。如果测试是用常量编写的,那么只能保证代码对特定值有效。如果测试随机地使构建服务器失败,则可能存在如何编写测试的问题。

随机数据将有助于确保未来的任何重构都不会依赖于一个神奇的常量。毕竟,如果您的测试是您的文档,那么常量的存在是否意味着它只需要为这些常量工作?

然而,我更喜欢将随机数据注入到我的测试中,作为“这个变量的值不应该影响这个测试的结果”的信号,这有些夸张。

我会说,如果你使用一个随机变量,然后叉你的测试基于该变量,那么这是一个气味。

你们应该问问自己你们测试的目的是什么。
单元测试 是关于验证逻辑、代码流和对象交互的。使用随机值试图实现不同的目标,从而减少了测试焦点和简单性。由于可读性原因(生成 UUID、 id、密钥等) ,它是可以接受的。
特别是对于单元测试,我甚至不记得这种方法曾经成功地发现过问题。但是我看到许多决定论问题(在测试中)试图在随机值和主要是随机日期方面变得更聪明。
对于 综合测试端到端测试端到端测试,模糊测试是一种有效的方法。

我赞成随机测试,并且我编写它们。但是,它们是否适合于特定的构建环境,以及它们应该包含在哪个测试套件中,这是一个更微妙的问题。

在本地运行(例如,在开发箱上过夜)随机测试发现了明显和模糊的 bug。那些晦涩难懂的东西非常神秘,我认为随机测试是唯一能够将它们排除在外的现实方法。作为一个测试,我通过随机测试发现了一个很难找到的错误,并让六个优秀的开发人员检查这个函数(大约十几行代码)发生的地方。没有人能够发现它。

你们反对随机数据的许多论点都是“该测试不可重复”的口味。然而,一个好的编写的随机测试将捕获用于启动随机种子的种子,并在失败时将其输出。除了允许您手动重复测试之外,这还允许您通过硬编码该测试的种子来创建测试特定问题的新测试。当然,手工编写覆盖该用例的显式测试可能更好,但是懒惰也有它的优点,这甚至允许您从一个失败的种子基本上自动生成新的测试用例。

但是,您提出的一点我不能争辩,那就是它破坏了构建系统。大多数构建和持续集成测试期望测试每次都做同样的事情。所以一个随机失败的测试将会制造混乱,随机失败并且指责那些无害的改变。

那么,一个解决方案是仍然运行随机测试作为构建和 CI 测试的一部分,但是 用固定的种子运行它,进行固定次数的迭代。因此,测试总是做同样的事情,但是仍然探索大量的输入空间(如果您运行它进行多次迭代)。

在本地,例如,当更改相关类时,您可以自由地运行它以进行更多迭代或使用其他种子。如果随机测试变得越来越流行,你甚至可以想象一套特定的测试,这些测试被认为是随机的,它们可以用不同的种子运行(因此随着时间的推移,覆盖范围越来越大) ,失败并不意味着与确定性 CI 系统相同的事情(例如,运行与代码变更没有1:1的关联,所以当事情失败时,你不会指责特定的变更)。

对于随机测试有很多要说的,特别是写得很好的测试,所以不要急于否定它们!

我认为这里的问题在于单元测试的目的不是捕捉 bug。目的是能够在不破坏代码的情况下修改代码,所以你怎么知道你破坏了 当您的随机单元测试在您的管道中是绿色的,只是因为它们没有触及正确的路径? 这么做对我来说太疯狂了。另一种情况可能是将它们作为集成测试或 e2e 运行,而不是作为构建的一部分,只是针对某些特定的情况,因为在某些情况下,您需要在断言中映射代码以进行测试。 拥有一个像您的真实代码一样复杂的测试套件就像根本没有测试一样,因为 那么要测试你的套房了吗

单元测试是为了确保响应特定输入时的正确行为,特别是应该涵盖所有代码路径/逻辑。没有必要使用随机数据来实现这一点。如果您的单元测试没有100% 的代码覆盖率,那么通过后门进行模糊测试是不会实现这一点的,这甚至可能意味着您偶尔不能实现所需的代码覆盖率。它可能(原谅我的双关语)给你一种“模糊”的感觉,你正在得到更多的代码路径,但可能没有太多的科学背后。人们经常在第一次运行单元测试时检查代码覆盖率,然后就忘记了(除非由 CI 强制执行) ,所以您真的希望因为使用随机输入数据而检查每次运行的代码覆盖率吗?这只是另一件可能被忽视的事情。

此外,程序员往往采取容易的道路,他们犯错误。它们在单元测试中犯的错误与在测试代码中犯的错误一样多。对于某些人来说,引入随机数据,然后在一次运行中根据输出顺序定制断言实在是太容易了。承认吧,我们都干过这种事。当数据发生变化时,顺序可能发生变化,断言失败,因此执行的一部分失败。这个部分不需要是1/2,我已经在10% 的失败中看到过这样的结果。追踪这样的问题需要很长时间,如果您的 CI 没有记录足够多的运行数据,那么情况可能会更糟。

虽然有一种观点认为“只是不要犯这些错误”,在一个典型的商业编程设置中,会有各种各样的能力,有时相对较低的人审查其他初级人员的代码。您可以在调试一个非确定性测试并修复它所需的时间内编写几十个测试,所以请确保您没有任何测试。不要使用随机数据。

根据我的经验,单元测试和随机测试应该分开。单元测试用于确定某些情况的正确性,而不仅仅是捕获模糊的错误。 尽管如此,随机测试是有用的,应该与单元测试分开进行,但它应该测试一系列的随机值。 我不禁认为,每次运行测试1个随机值是不够的,既不能成为一个充分的随机测试,也不能成为一个真正有用的单元测试。

另一个方面是验证测试结果。如果有随机输入,则必须在测试中计算它的预期输出。这将在某种程度上重复测试逻辑,使测试只是测试代码本身的镜像。这将不能充分测试代码,因为测试可能包含与原始代码相同的错误。

这是一个老问题,但是我想提一下我创建的一个库,它可以生成填充了随机数据的对象。如果测试通过提供种子失败,它支持重新生成相同的数据。它还通过扩展支持 JUnit 5。

示例用法:

Person person = Instancio.create(Person.class);

或者定制生成参数的构建器 API:

Person person = Instancio.of(Person.class)
.generate(field("age"), gen -> gen.ints.min(18).max(65))
.create();

Github 链接有更多的例子: https://github.com/instancio/instancio

你可以在 Maven 中心找到图书馆:

<dependency>
<groupId>org.instancio</groupId>
<artifactId>instancio-junit</artifactId>
<version>LATEST</version>
</dependency>