使用嵌套的 forAll 在 ScalaCheck 中生成的对象之间共享元素

Sharing elements between generated objects in ScalaCheck using nested forAll

最近开始使用 Scala 编码,我尝试编写一些基于 属性 的测试用例。在这里,我试图生成模拟我正在测试的系统的原始数据。目标是首先生成基本元素(ctrlidz),然后使用这些值生成两个 类(A1B1),最后检查他们的财产。我首先尝试了以下 -

import org.scalatest._
import prop._
import scala.collection.immutable._
import org.scalacheck.{Gen, Arbitrary}

case class A(
    controller: String,
    id: Double,
    x: Double
)

case class B(
    controller: String,
    id: Double,
    y: Double
)

object BaseGenerators {
    val ctrl = Gen.const("ABC")
    val idz = Arbitrary.arbitrary[Double]
}

trait Generators {
    val obj = BaseGenerators

    val A1 = for {
        controller <- obj.ctrl
        id <- obj.idz
        x <- Arbitrary.arbitrary[Double]
    } yield A(controller, id, x)

    val B1 = for {
        controller <- obj.ctrl
        id <- obj.idz
        y <- Arbitrary.arbitrary[Double]
    } yield B(controller, id, y)

}

class Something extends PropSpec with PropertyChecks with Matchers with Generators{

    property("Controllers are equal") {
        forAll(A1, B1) {
            (a:A,b:B) => 
                a.controller should be (b.controller)
        }
    }

    property("IDs are equal") {
        forAll(A1, B1) {
            (a:A,b:B) => 
                a.id should be (b.id)
        }
    }

}

运行 sbt test 在终端给了我以下 -

[info] Something:
[info] - Controllers are equal
[info] - IDs are equal *** FAILED ***
[info]   TestFailedException was thrown during property evaluation.
[info]     Message: 1.1794559135007427E-271 was not equal to 7.871712821709093E212
[info]     Location: (testnew.scala:52)
[info]     Occurred when passed generated values (
[info]       arg0 = A(ABC,1.1794559135007427E-271,-1.6982696700585273E-23),
[info]       arg1 = B(ABC,7.871712821709093E212,-8.820696498155311E234)
[info]     )

现在很容易看出为什么第二个 属性 失败了。因为每次我产生 A1B1 我都会为 id 而不是 ctrl 产生不同的值,因为它是一个常数。以下是我的第二种方法,其中我创建嵌套 for-yield 来尝试实现我的目标 -

case class Popo(
    controller: String,
    id: Double,
    someA: Gen[A],
    someB: Gen[B]
)

trait Generators {
    val obj = for {
        ctrl <- Gen.alphaStr
        idz <- Arbitrary.arbitrary[Double]
        val someA = for {
            x <- Arbitrary.arbitrary[Double]
        } yield A(ctrl, idz, someA)
        val someB = for {
            y <- Arbitrary.arbitrary[Double]
        } yield B(ctrl, idz, y)
    } yield Popo(ctrl, idz, x, someB)
}

class Something extends PropSpec with PropertyChecks with Matchers with Generators{

    property("Controllers are equal") {
        forAll(obj) {
            (x: Popo) => 
            forAll(x.someA, x.someB) {
                (a:A,b:B) => 
                    a.controller should be (b.controller)
            }
        }
    }

    property("IDs are equal") {
        forAll(obj) {
            (x: Popo) =>
            forAll(x.someA, x.someB) {
                (a:A,b:B) => 
                    a.id should be (b.id)
            }
        }
    }
}

运行 sbt test 第二种方法告诉我所有测试都通过了。

[info] Something:
[info] - Controllers are equal
[info] - IDs are equal
[info] ScalaTest
[info] Run completed in 335 milliseconds.
[info] Total number of tests run: 2
[info] Suites: completed 1, aborted 0
[info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0
[info] All tests passed.

是否有 better/alternative 方法来重现我想要的结果?嵌套 forAll 对我来说似乎相当笨拙。如果我的对象共享元素的依赖关系图中有 R -> S -> ... V -> W,那么我将不得不创建尽可能多的嵌套 forAll.

我将在 Scalacheck 中给出答案。我知道 Scalatest 很受欢迎,但我发现它包含在一个关于 Scalacheck 的问题中会让人分心,尤其是当没有理由没有它就无法编写示例时。

你似乎想测试 AB,但他们共享信息。表示该依赖关系的一种方法是您编写的 Popo class。它包含 AB 的共享信息和生成值。另一种选择是在 class 中生成 AB 之间的共享值。

最简单的解决方案是成对生成 AB(二元组)。 不幸的是,有一些技巧可以让它发挥作用。您将需要在 forAll 属性 中使用 case 关键字。您无法为 Arbitrary 元组提供 implicit 值的证据,因此您 必须 forAll 中明确指定元组的生成器.

import org.scalacheck.Gen
import org.scalacheck.Arbitrary
import org.scalacheck.Prop
import org.scalacheck.Prop.AnyOperators
import org.scalacheck.Properties

case class A(
  controller: String,
  id: Double,
  x: Double
)

case class B(
  controller: String,
  id: Double,
  y: Double
)

object BaseGenerators {
  val ctrl = Gen.const("ABC")
  val idz = Arbitrary.arbitrary[Double]
}

object Generators {
  val obj = BaseGenerators

  val genAB: Gen[(A,B)] = for {
    controller <- obj.ctrl
    id <- obj.idz
    x <- Arbitrary.arbitrary[Double]
    y <- Arbitrary.arbitrary[Double]
    val a = A(controller, id, x)
    val b = B(controller, id, y)
  } yield (a, b)                                         // !
}

class Something extends Properties("Something") {

  property("Controllers and IDs are equal") = {
    Prop.forAll(Generators.genAB) { case (a: A, b: B) => // !
      a.controller ?= b.controller && a.id ?= b.id
    }
  }
}

关于您关于让对象共享信息的更广泛的问题,您可以通过编写带有函数参数的生成器来表示它。但是,它仍然需要嵌套 forAll 个生成器。

object Generators {
  val obj = BaseGenerators

  val genA = for {
    controller <- obj.ctrl
    id <- obj.idz
    x <- Arbitrary.arbitrary[Double]
  } yield A(controller, id, x)

  def genB(a: A) = for {                                 // !
    y <- Arbitrary.arbitrary[Double]
  } yield B(a.controller, a.id, y)
}

class Something extends Properties("Something") {

  implicit val arbA: Arbitrary[A] = Arbitrary {
    Generators.genA
  }

  property("Controllers and IDs are equal") = {
    Prop.forAll { a: A =>                                // !
      Prop.forAll(Generators.genB(a)) { b: B =>          // !
        (a.controller ?= b.controller) && (a.id ?= b.id)
      }
    }
  }
}