使用嵌套的forAll

时间:2019-05-31 03:47:10

标签: scala testing scalatest scalacheck property-based-testing

相当早以前就开始在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.

是否有更好的/替代的方法来再现我想要的结果?对我来说,嵌套forAll似乎很笨拙。如果要在我的依赖图中将R -> S -> ... V -> W用于对象共享元素,则必须创建尽可能多的嵌套forAll

1 个答案:

答案 0 :(得分:2)

我将仅在Scalacheck中给出答案。我知道Scalatest很受欢迎,但是我发现它包含在一个有关Scalacheck的问题中,这会分散注意力,尤其是当没有理由没有该示例无法编写示例时。

似乎您想测试AB,但是它们共享信息。表示这种依赖性的一种方法是您编写的Popo类。它既包含共享信息,又包含AB的生成值。另一种选择是在类中的AB之间生成共享值。

最简单的解决方案是成对产生AB(两个元组)。  不幸的是,有一些技巧可以使它起作用。您将需要在case属性中使用forAll关键字。您无法提供implicit元组的Arbitrary值的证据,因此您必须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)
      }
    }
  }
}