scalaintermediate

Property-Based Testing with ScalaCheck

Write property-based tests with ScalaCheck: generators, properties, shrinking, and custom Arbitrary.

scala
import org.scalacheck.*
import org.scalacheck.Prop.*
import org.scalacheck.Gen

// Basic properties
object StringProps extends Properties("String"):
  property("startsWith") = forAll { (a: String, b: String) =>
    (a + b).startsWith(a)
  }

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

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

  property("substring") = forAll { (a: String, b: String, c: String) =>
    (a + b + c).contains(b)
  }

// List properties
object ListProps extends Properties("List"):
  property("reverse reverse") = forAll { (l: List[Int]) =>
    l.reverse.reverse == l
  }

  property("sort is sorted") = forAll { (l: List[Int]) =>
    val sorted = l.sorted
    sorted.zip(sorted.tail).forall((a, b) => a <= b) || sorted.length <= 1
  }

  property("filter subset") = forAll { (l: List[Int]) =>
    l.filter(_ > 0).forall(_ > 0)
  }

  property("map preserves length") = forAll { (l: List[Int]) =>
    l.map(_ * 2).length == l.length
  }

// Custom generators
case class Email(local: String, domain: String):
  override def toString = s"$local@$domain"

case class User(name: String, age: Int, email: Email)

object Generators:
  val genEmail: Gen[Email] = for
    local  <- Gen.alphaNumStr.suchThat(_.nonEmpty)
    domain <- Gen.oneOf("test.com", "example.org", "mail.io")
  yield Email(local, domain)

  val genUser: Gen[User] = for
    name  <- Gen.alphaStr.suchThat(s => s.nonEmpty && s.length <= 50)
    age   <- Gen.choose(0, 120)
    email <- genEmail
  yield User(name, age, email)

  val genEvenInt: Gen[Int] = Gen.choose(1, 1000).map(_ * 2)
  val genPosInt: Gen[Int] = Gen.posNum[Int]
  val genColor: Gen[String] = Gen.oneOf("red", "green", "blue", "yellow")

  given Arbitrary[Email] = Arbitrary(genEmail)
  given Arbitrary[User] = Arbitrary(genUser)

object UserProps extends Properties("User"):
  import Generators.given

  property("age in range") = forAll { (user: User) =>
    user.age >= 0 && user.age <= 120
  }

  property("email has @") = forAll { (user: User) =>
    user.email.toString.contains("@")
  }

  property("name non-empty") = forAll { (user: User) =>
    user.name.nonEmpty
  }

object MathProps extends Properties("Math"):
  import Generators.*

  property("even * even = even") = forAll(genEvenInt, genEvenInt) {
    (a: Int, b: Int) =>
      (a.toLong * b.toLong) % 2 == 0
  }

  property("abs non-negative") = forAll { (n: Int) =>
    n == Int.MinValue || Math.abs(n) >= 0
  }

Use Cases

  • Exhaustive testing with random inputs
  • Finding edge cases automatically
  • Specification-driven testing

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.