Unit testing 随机数据的单元测试

Unit testing 随机数据的单元测试,unit-testing,testing,tdd,Unit Testing,Testing,Tdd,我读过,在单元测试中生成随机数据通常是一个坏主意(我也理解为什么),但是在随机数据上进行测试,然后从随机测试中构建一个固定的单元测试用例,这样可以发现bug,这看起来不错。然而,我不明白如何很好地组织它。我的问题实际上与特定的编程语言或特定的单元测试框架无关,所以我将使用python和一些伪单元测试框架。以下是我对编码的看法: def random_test_cases(): datasets = [ dataset1, dataset2, ...

我读过,在单元测试中生成随机数据通常是一个坏主意(我也理解为什么),但是在随机数据上进行测试,然后从随机测试中构建一个固定的单元测试用例,这样可以发现bug,这看起来不错。然而,我不明白如何很好地组织它。我的问题实际上与特定的编程语言或特定的单元测试框架无关,所以我将使用python和一些伪单元测试框架。以下是我对编码的看法:

def random_test_cases():
   datasets = [
       dataset1,
       dataset2,
       ...
       datasetn
   ]
   for dataset in datasets:
       assertTrue(...)
       assertEquals(...)
       assertRaises(...)
       # and so on
问题是:当这个测试用例失败时,我无法找出是哪个数据集导致了失败。我认为有两种解决方法:

  • 为每个数据集创建一个测试用例-问题是测试用例的负载和代码重复
  • 通常,测试框架允许我们将消息传递给断言函数(在我的示例中,我可以执行类似于
    assertTrue(…,message=str(dataset))
    )。问题是我应该将这样的消息传递给每个断言,这看起来并不优雅

  • 有更简单的方法吗?

    您可以通过扩展而不是枚举来定义测试,或者您可以从单个用例调用多个测试用例

    从单个测试用例调用多个测试用例:

    MyTest()
    {
        MyTest(1, "A")
        MyTest(1, "B")
        MyTest(2, "A")
        MyTest(2, "B")
        MyTest(3, "A")
        MyTest(3, "B")
    }
    
    有时有一些优雅的方法可以通过一些测试框架来实现这一点。是如何在NUnit中实现的:

    [Test, Combinatorial]
    public void MyTest(
        [Values(1,2,3)] int x,
        [Values("A","B")] string s)
    {
        ...
    }
    

    我仍然认为这是个坏主意

    单元测试需要简单明了。给定相同的代码和相同的单元测试,您应该能够无限地运行它,并且永远不会得到不同的响应,除非有外部因素在起作用。与此相反的目标将增加自动化的维护成本,这与目的背道而驰

    除了维护方面,对我来说,它似乎很懒惰。如果您考虑到您的功能并理解正面和负面的测试用例,那么开发单元测试是很简单的


    我也不同意用户演示如何在同一个测试用例中执行多个测试用例。当测试失败时,您应该能够立即知道哪个测试失败,并知道失败的原因。测试应该尽可能简单,并且尽可能简洁/与被测代码相关。

    通常,只要选择正确的断言方法,单元测试框架就支持“信息性失败”

    但是,如果其他一切都不起作用,您可以轻松地将数据集跟踪到控制台/输出文件。技术含量低,但应该有效

    [TestCaseSource("GetDatasets")]
    public Test.. (Dataset d)
    {
       Console.WriteLine(PrettyPrintDataset(d));
       // proceed with checks
       Console.WriteLine("Worked!");
    }
    

    我也认为这是个坏主意

    请注意,不要向代码中抛出随机数据,而是让单元测试这样做。这一切都归结为为什么您首先要进行单元测试。答案是“推动代码的设计”。随机数据不会驱动代码的设计,因为它依赖于非常严格的公共接口。请注意,您可以通过它找到bug,但这不是单元测试的目的。让我注意到,我说的是单元测试,而不是一般的测试

    话虽如此,我还是强烈建议大家看一看。这是Haskell,所以在演示和文档方面有点狡猾,但你应该能够弄清楚。不过,我要总结一下它是如何工作的

    在选择要测试的代码(比如说
    sort()
    函数)之后,就建立了应该保存的不变量。在本例中,如果
    result=sort(input)
    ,则可以使用以下不变量:

    • 结果中的每个元素应小于或等于下一个元素
    • 输入
      中的每个元素应在
      结果
      中出现相同的次数
    • 结果
      输入
      应该具有相同的长度(这是重复前面的内容,但让我们用它来说明)
    在一个简单的函数中对每个变量进行编码,该函数获取结果和输出,并检查这些不变量是否编码

    然后,告诉QuickCheck如何生成
    输入
    。由于这是Haskell,并且类型系统非常棒,它可以看到函数接受一个整数列表,并且知道如何生成这些整数。它基本上生成随机整数和随机长度的随机列表。当然,如果您有一个更复杂的数据类型(例如,只有正整数,只有平方等),那么它可以更细粒度

    最后,当你有这两个,你只需运行快速检查。它随机生成所有这些内容并检查不变量。如果有些失败了,它会告诉你到底是哪一个。它还将告诉您随机种子,因此如果需要,您可以重新运行此失败。作为额外的奖励,每当它得到一个失败的不变量时,它将尝试将输入减少到使不变量失败的最小可能子集(如果你想到一个树结构,它将把它减少到使不变量失败的最小子树)


    就在这里。在我看来,这就是你应该如何用随机数据来测试东西。这绝对不是单元测试,我甚至认为您应该以不同的方式运行它(比如,让CI时不时地运行它,而不是每次更改都运行它(因为它会很快变慢))。让我重复一遍,单元测试的另一个好处是快速检查可以发现bug,而单元测试可以驱动设计。

    在R的快速检查中,我们试图解决这个问题,如下所示

    • 测试实际上是伪随机的(种子是固定的),因此您可以随时复制测试结果(当然,排除外部因素)
    • test
      函数返回足够的数据来重现错误,包括失败的断言和导致错误的数据。对
      test
      的返回值调用一个方便的函数
      repro
      ,将在失败断言开始时将您放入调试器,并将参数设置为失败的见证人。如果在批处理模式下执行测试,则等效