Сделать тесты ScalaCheck детерминированными

Я хотел бы сделать мои тесты свойств ScalaCheck в моем тестовом наборе specs2 детерминированным, временно, чтобы облегчить отладку. В настоящее время различные значения могут генерироваться каждый раз при повторном запуске набора тестов, что делает отладку расстраивающей, потому что вы не знаете, вызвано ли изменение наблюдаемого поведения вашими изменениями кода или просто сгенерированными разными данными.

Как я могу это сделать? Есть ли официальный способ установить случайное семя, используемое ScalaCheck?

Я использую sbt для запуска набора тестов.

Бонусный вопрос: Есть ли официальный способ распечатать случайное семя, используемое ScalaCheck, чтобы вы могли воспроизвести даже недетерминированный тестовый прогон?

Ответы

Ответ 1

Если вы используете чистые свойства 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)

Ответ 2

Как правило, при тестировании на недетерминированных входах вы должны попытаться эхо или сохранить эти входы где-нибудь там, где произошел сбой.

Если данные малы, вы можете включить их в ярлык или сообщение об ошибке, которое будет показано пользователю; например, в тесте xUnit-стиля: (поскольку я новичок в синтаксисе Scala)

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

Если данные велики, например, автоматически сгенерированный БД, вы можете либо сохранить его в энергонезависимом месте (например,/tmp с именем с меткой времени), либо показать семя, используемое для его создания.

Следующий шаг важен: возьмите это значение или семя или что-то еще, и добавьте его в свои детерминированные тесты регрессии, чтобы он проверялся каждый раз с этого момента.

Вы говорите, что хотите сделать ScalaCheck детерминированным "временно" для воспроизведения этой проблемы; Я говорю, что вы нашли багги с краем, который хорошо подходит для того, чтобы стать unit test (возможно, после некоторого упрощения вручную).

Ответ 3

Дополнительный вопрос: существует ли официальный способ распечатать случайное начальное число, используемое 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

Чтобы воспроизвести этот конкретный прогон (т. scalacheck.seed С тем же начальным scalacheck.seed), вы можете взять 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_== "")
}

Вот документация обо всех этих различных способах. Этот блог также говорит об этом.

Ответ 4

Для scalacheck-1.12 эта конфигурация работала:

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

Для scalacheck-1.13 он больше не работает, поскольку метод rng удален. Любые мысли?