使ScalaCheck测试具有确定性

16

我希望能使我的ScalaCheck属性测试在specs2测试套件中是暂时确定性的,以便于调试。目前,每次重新运行测试套件时可能会生成不同的值,这使得调试非常困难,因为你无法确定观察到的行为变化是由你的代码更改引起的,还是只是由于生成了不同的数据。

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

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

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


这些变化的数据来自哪里?数据库?服务器? - Brian Agnew
@BrianAgnew 不,这是由 ScalaCheck 随机生成的。 - Robin Green
4个回答

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
“使用纯ScalaCheck属性”是什么意思?你的意思是不在其中使用任何specs2的东西吗? - Robin Green
1
我指的是使用ScalaCheck规范:object StringSpecification extends Properties("String"),如ScalaCheck用户指南所示:https://github.com/rickynils/scalacheck/wiki/User-Guide - Eric

1
作为一般规则,在测试非确定性输入时,当出现失败时,应该尝试回显或保存这些输入的某些信息。
如果数据很小,您可以将其包含在向用户显示的标签或错误消息中;例如,在xUnit样式的测试中:(由于我对Scala语法不熟悉)
testLength(String x) {
    assert(x.length > 10, "Length OK for '" + x + "'");
}

如果数据很大,例如自动生成的数据库,您可以将其存储在非易失性位置(例如带有时间戳名称的/tmp),或显示用于生成它的种子。
下一步非常重要:将该值、种子或其他内容添加到确定性回归测试中,以便从现在开始每次都进行检查。
您说您想要暂时使ScalaCheck变得确定性,以重现此问题;我认为您已经找到了一个适合成为单元测试的错误边缘情况(可能需要手动简化)。

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_== "")
}

这里是有关所有这些不同方法的文档。 这个博客也谈到了这个问题。


为什么使用sbt?在许多项目中是被禁止的。 - tribbloid
为什么使用sbt?在许多项目中是被禁止的。 - undefined

0

对于 scalacheck-1.12,这个配置起作用:

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接