Skip to main content

Migration From ScalaCheck

Migration From ScalaCheck

For many cases migrating from ScalaCheck to Hedgehog should be fairly straight forward, as the general principals are quite similar, and the changes are largely syntactic.

Properties

Some basic rules:

  • Replace Properties("...") with just Properties

  • Replace Prop.forAll with a call to forAll on a specific Gen instance

    • If you have previously been relying on Arbitrary instances these have to be replaced with calls to functions that return an instance of Gen.

      See the extra package for some stand Scala data type combinators.

      For more information see the section on Gen.

  • flatMap over the result of your genFoo.forAll, or use a for comprehension.

  • Return your Prop or Boolean assetions with Result.assert(...)

  • Replace label or :| with Result.log(...)

  • Replace equality assertions like ?= with ====

ScalaCheck

import org.scalacheck._

object StringSpecification extends Properties("String") {

property("startsWith") =
Prop.forAll { (a: String, b: String) =>
(a+b).startsWith(a)
}
}

Hedgehog

import hedgehog._
import hedgehog.runner._

object StringSpecification extends Properties {

override def tests: List[Test] = List(
property("startsWith", for {
a <- Gen.string(Gen.unicode, Range.linear(0, 100)).forAll
b <- Gen.string(Gen.unicode, Range.linear(0, 100)).forAll
} yield Result.assert((a+b).startsWith(a))
)
)
}

Gen

Some basic rules:

  • Gen.list and Gen.listOfN can be replaced with a call to list(Range.linear(0, n)) on a specific Gen instance.
  • Gen.const is now Gen.constant
  • Arbitrary.arbitrary[Int] is now Gen.int(Range.linear(min, max))
  • Gen.oneOf is now Gen.choice1

It's important to note that there are no more "default" Arbitrary instances to summon. You must decided what kind of int or String you want to generate, and what their Range is.

ScalaCheck

val genLeaf = Gen.const(Leaf)

val genNode = for {
v <- arbitrary[Int]
left <- genTree
right <- genTree
} yield Node(left, right, v)

def genTree: Gen[Tree] = Gen.oneOf(genLeaf, genNode)

Hedgehog

val genLeaf = Gen.constant(Leaf)

val genNode = for {
v <- Gen.int(Range.linear(Integer.MaxValue, Integer.MinValue))
left <- genTree
right <- genTree
} yield Node(left, right, v)

def genTree: Gen[Tree] = Gen.choice1(genLeaf, genNode)

Arbitrary

Some basic rules:

  • Replace implict def functions that return Arbitrary to a function that returns the Gen directly.

ScalaCheck

This example was taken from the ScalaCheck Guide.

implicit def arbTree[T](implicit a: Arbitrary[T]): Arbitrary[Tree[T]] = Arbitrary {

val genLeaf = for(e <- Arbitrary.arbitrary[T]) yield Leaf(e)

def genInternal(sz: Int): Gen[Tree[T]] = for {
n <- Gen.choose(sz/3, sz/2)
c <- Gen.listOfN(n, sizedTree(sz/2))
} yield Internal(c)

def sizedTree(sz: Int) =
if(sz <= 0) genLeaf
else Gen.frequency((1, genLeaf), (3, genInternal(sz)))

Gen.sized(sz => sizedTree(sz))
}
def genTree[T](g: Gen[T]): Gen[Tree[T]] = {

val genLeaf = for(e <- g) yield Leaf(e)

def genInternal(sz: Size): Gen[Tree[T]] = for {
n <- Gen.choose(sz.value/3, sz.value/2)
c <- sizedTree(sz.value/2).list(Range.linear(0, n))
} yield Internal(c)

def sizedTree(sz: Size) =
if(sz.value <= 0) genLeaf
else Gen.frequency1((1, genLeaf), (3, genInternal(sz)))

Gen.sized(sz => sizedTree(sz))
}

Shrink

This is assuming you're even writing them in the first place...

ScalaCheck

case class Data(a: String, i: Int)

implicit def arbData: Arbitrary[Data] =
Arbitrary[Data] {
for {
s <- arbitrary[String]
i <- arbitrary[Int]
} yield Data(a, i)
}

implicit def shrink: Shrink[Data] =
Shrink[Data] { case Data(a, i) =>
shrink(a).map(a2 => Data(a2, i)) append
shrink(i).map(i2 => Data(a, i2))
}

Hedgehog

Good news, you don't need to do anything! Just write your generators.

def genData: Gen[Data] =
for {
s <- Gen.string(Gen.unicode, Range.linear(0, 100))
i <- Gen.int(Range.linear(-100, 100))
} yield Data(a, i)