如何在scalatest中使用forAll只生成生成器的一个对象?

how to use forAll in scalatest to generate only one object of a generator?

我正在使用 scalatest 和 scalacheck,同时也在使用 FeatureSpec。

我有一个生成器 class 可以为我生成如下所示的对象:

object InvoiceGen {

  def myObj = for {

    country <- Gen.oneOf(Seq("France", "Germany", "United Kingdom", "Austria"))
    type <- Gen.oneOf(Seq("Communication", "Restaurants", "Parking"))
    amount <- Gen.choose(100, 4999)
    number <- Gen.choose(1, 10000)
    valid <- Arbitrary.arbitrary[Boolean]

  } yield SomeObject(country, type, "1/1/2014", amount,number.toString, 35, "something", documentTypeValid, valid, "")

现在,我有了测试 class,它可以与 FeatureSpec 一起工作,以及 运行 测试所需的一切。

在这个class中我有场景,在每个场景中我想生成一个不同的对象。 据我了解,生成对象最好使用 forAll func,但 for all 不一定会给你带来一个对象,所以你可以添加 minSuccessful(1) 以确保你得到列表 1 obj ... .

我是这样做的并且有效:

scenario("some scenario") {
      forAll(MyGen.myObj, minSuccessful(1)) { someObject =>
        Given("A connection to the system")
        loginActions shouldBe 'Connected

        When("something")
        //blabla
        Then("something should happened")
        //blabla
      }
    }

但我不确定它到底是什么意思。 我想要的是为每个场景生成一张发票并对其执行一些操作...... 我不确定为什么我关心生成是否有效...我只是想要一个生成的对象来使用。

我认为您想要使用 Scalacheck 的方式(只生成一个对象并为其执行测试)违背了基于 属性 的测试的目的。让我详细解释一下:

经典单元测试中,您将在测试系统下生成您的系统,无论是对象还是依赖对象系统,有一些固定的数据。这可以例如是像 "foo" 和 "bar" 这样的字符串,或者,如果你需要一个名字,你可以使用像 "John Doe" 这样的字符串。对于整数等数据,也可以随机取一些值。

主要优点是这些是 "plain" 值——您可以直接在代码中看到它们并将它们与失败测试的输出相关联。最大的缺点是测试只会 运行 使用您指定的值,这反过来意味着 您的代码也只使用这些值进行测试 .

相比之下,基于 属性 的测试 允许您仅描述数据的外观(例如 "a positive integer"、"a string of maximum 20 characters" ).然后,测试框架将在生成器的帮助下生成多个匹配对象并为所有对象执行测试。这样,您可以更加确定您的代码对于不同的输入 实际上是正确的,毕竟这是测试的目的:检查您的代码是否按照可能的情况执行了它应该做的事情输入。

我从未真正使用过 Scalacheck,但一位同事向我解释说它还试图涵盖边缘情况,例如为正整数输入 0 和 MAX_INT,或为上述最大字符串输入空字符串。 20 个字符。

因此,总结一下:运行 基于 属性 的测试只对一个通用对象进行一次是错误的做法。相反,一旦您拥有生成器基础架构,就可以利用您所拥有的优势,让您的代码被检查更多次!

TL;DR:要获得一个对象,并且只有一个,请使用 myObj.sample.get。除非你的发电机正在做一些非常安全且不会爆炸的奇特事情。

我推测您的意图是 运行 某种 integration/acceptance 测试一些随机生成的领域对象——换句话说(滥用)使用 scalacheck 作为一个简单的数据生成器——并且你希望 minSuccessful(1) 能确保测试只 运行 一次。

请注意情况并非如此!。如果失败,scalacheck 将 运行 你的测试多次,以尝试将输入数据缩小到最小的反例。

如果您希望确保您的测试 运行 只进行一次,您 必须 使用 sample.

但是,如果 运行 多次测试没问题,更喜欢 minSuccessful(1) 而不是 "succeed fast",但在测试失败的情况下仍然可以从最小化的反例中获益。


Gen.sample return 是一个选项,因为 generators can fail:

ScalaCheck generators can fail, for instance if you're adding a filter (listingGen.suchThat(...)), and that failure is modeled with the Option type.

但是:

[…] if you're sure that your generator never will fail, you can simply call Option.get like you do in your example above. Or you can use Option.getOrElse to replace None with a default value.

一般来说,如果您的生成器很简单,即不使用可能会失败的生成器并且不单独使用任何过滤器,那么在选项 returned 上调用 .get 是完全安全的通过 .sample。我过去一直这样做,从来没有遇到过问题。如果您的生成器经常从 .sample return None 它们可能会使 scalacheck 也无法成功生成值。

如果您只需要一个对象,请使用 Gen.sample.get.


minSuccessful 有一个非常不同的含义:它是 最小 的成功测试数量,可扩展检查 运行s—— 没有意味着 意味着

  • scalacheck 只从生成器中获取一个值,或者
  • 测试 运行 只有一次。

使用 minSuccessful(1) scalacheck 想要一个成功的测试。它将从生成器中取出样本,直到测试 运行s 至少一次——即如果您在测试主体中使用 whenever 过滤生成的值,只要 whenever 丢弃它们,scalacheck 就会采集样本。

如果测试通过,scalacheck 很高兴,不会 运行 第二次测试。

但是,如果测试 失败, scalacheck 将尝试生成一个最小示例来使测试失败。它会 shrink 输入数据和 运行 测试,只要它失败,然后为您提供 minimized 反例比触发初始故障的实际输入。

这是 属性 测试的一个重要 属性,因为它可以帮助您发现错误:原始数据通常太大而无法自行调试。最小化它可以帮助您发现实际触发故障的输入数据,即您没有想到的空字符串等极端情况。