使ScalaCheck测试确定性

时间:2012-09-28 11:49:29

标签: scala specs2 scalacheck

我想暂时在我的specs2测试套件中进行ScalaCheck属性测试,以便于调试。现在,每次重新运行测试套件时都会生成不同的值,这会使调试变得令人沮丧,因为您不知道观察到的行为的变化是由代码更改引起的,还是仅仅是由生成的不同数据引起的。

我该怎么做?是否有正式的方法来设置ScalaCheck使用的随机种子?

我正在使用sbt来运行测试套件。

奖金问题 是否有正式方法打印 ScalaCheck使用的随机种子,以便您可以重现甚至是非确定性的测试运行?

4 个答案:

答案 0 :(得分:11)

如果您正在使用纯ScalaCheck属性,那么您应该能够使用Test.Params类来更改所使用的java.util.Random实例并提供您自己的实例,它始终返回相同的值集:

def check(params: Test.Parameters, p: Prop): Test.Result

[更新]

我刚发布了一个新的specs2-1.12.2-SNAPSHOT,您可以使用以下语法指定随机生成器:

case class MyRandomGenerator() extends java.util.Random {
  // implement a deterministic generator 
}

"this is a specific property" ! prop { (a: Int, b: Int) =>
  (a + b) must_== (b + a)
}.set(MyRandomGenerator(), minTestsOk -> 200, workers -> 3)

答案 1 :(得分:1)

作为一般规则,在对非确定性输入进行测试时,您应该尝试在出现故障时回显或保存这些输入。

如果数据很小,您可以将其包含在向用户显示的标签或错误消息中;例如,在xUnit风格的测试中:(因为我是Scala语法的新手)

testLength(String x) {
    assert(x.length > 10, "Length OK for '" + x + "'");
}

如果数据很大,例如自动生成的数据库,您可以将其存储在非易失性位置(​​例如带有带时间戳名称的/ tmp)或显示用于生成它的种子。

下一步很重要:获取该值,种子或其他内容,并将其添加到确定性回归测试中,以便从现在开始每次都检查它。

你说你想让ScalaCheck确定性“暂时”重现这个问题;我说你发现了一个有缺陷的边缘案例,非常适合成为一个单元测试(也许是在一些手动简化之后)。

答案 2 :(得分:1)

  

奖励问题:是否有官方方法可以打印出ScalaCheck使用的随机种子,以便您甚至可以重现不确定的测试运行?

specs2-scalacheck版本4.6.0开始,这现在是默认行为:

给出测试文件HelloSpec

package example

import org.specs2.mutable.Specification
import org.specs2.ScalaCheck

class HelloSpec extends Specification  with ScalaCheck {
package example

import org.specs2.mutable.Specification
import org.specs2.ScalaCheck

class HelloSpec extends Specification  with ScalaCheck {
  s2"""
    a simple property       $ex1
  """

  def ex1 = prop((s: String) => s.reverse.reverse must_== "")
}

build.sbt配置:

import Dependencies._

ThisBuild / scalaVersion     := "2.13.0"
ThisBuild / version          := "0.1.0-SNAPSHOT"
ThisBuild / organization     := "com.example"
ThisBuild / organizationName := "example"

lazy val root = (project in file("."))
  .settings(
    name := "specs2-scalacheck",
    libraryDependencies ++= Seq(
      specs2Core,
      specs2MatcherExtra,
      specs2Scalacheck
    ).map(_ % "test")
  )

project/Dependencies

import sbt._

object Dependencies {
  lazy val specs2Core                       = "org.specs2"             %% "specs2-core"               % "4.6.0"
  lazy val specs2MatcherExtra               = "org.specs2"             %% "specs2-matcher-extra"      % specs2Core.revision
  lazy val specs2Scalacheck                 = "org.specs2"             %% "specs2-scalacheck"         % specs2Core.revision

}

sbt控制台运行测试时:

sbt:specs2-scalacheck> testOnly example.HelloSpec

您将获得以下输出:

[info] HelloSpec
[error]     x a simple property
[error]  Falsified after 2 passed tests.
[error]  > ARG_0: "\u0000"
[error]  > ARG_0_ORIGINAL: "猹"
[error]  The seed is X5CS2sVlnffezQs-bN84NFokhAfmWS4kAg8_gJ6VFIP=
[error]  
[error]  > '' != '' (HelloSpec.scala:11)
[info] Total for specification HelloSpec

要重现特定的运行(即具有相同的种子),您可以从输出中获取seed,并使用命令行scalacheck.seed传递它:

sbt:specs2-scalacheck>testOnly example.HelloSpec -- scalacheck.seed X5CS2sVlnffezQs-bN84NFokhAfmWS4kAg8_gJ6VFIP=

这将产生与以前相同的输出。

您还可以使用setSeed以编程方式设置种子:

def ex1 = prop((s: String) => s.reverse.reverse must_== "").setSeed("X5CS2sVlnffezQs-bN84NFokhAfmWS4kAg8_gJ6VFIP=")

提供Seed的另一种方法是传递设置Parameters的隐式seed

package example

import org.specs2.mutable.Specification
import org.specs2.ScalaCheck
import org.scalacheck.rng.Seed
import org.specs2.scalacheck.Parameters

class HelloSpec extends Specification  with ScalaCheck {

  s2"""
    a simple property       $ex1
  """

  implicit val params = Parameters(minTestsOk = 1000, seed = Seed.fromBase64("X5CS2sVlnffezQs-bN84NFokhAfmWS4kAg8_gJ6VFIP=").toOption)

  def ex1 = prop((s: String) => s.reverse.reverse must_== "")
}

Here是有关所有这些各种方式的文档。 这个blog也谈到了这一点。

答案 3 :(得分:0)

对于scalacheck-1.12,此配置有效:

new Test.Parameters {
  override val rng = new scala.util.Random(seed)
}

对于scalacheck-1.13,由于删除了rng方法,它不再起作用。有什么想法吗?