PhD
PhD

Reputation: 11334

How can I get random data generated for scala case classes with the ability to "change some values" for unit testing?

I'm working with a piece of code that has a broad/deep case class hierarchy. For unit testing, I'd like to have "random data" populated in the classes with the ability to change the data for the fields I care about?

Example:

case class Foo(bar: Bar, name: String, value: Int)
case class Bar(baz: Baz, price: Double)
case class Baz(thing: String)

So something like:

val randomFoo = GenerateRandomData(Foo)
randomFoo.bar.baz = Baz("custom for testing")

I've heard of ScalaCheck and Shapeless and Scalacheck-shapeless and they do provide some sort of random data generation but nothing with customization it seems.

I'm currently using ScalaMock but that builds out null fields and breaks testability for "other" tests. I used something similar in .Net like Auto Fixture and was wondering if there was something similar in Scala.

Upvotes: 4

Views: 2204

Answers (3)

ashawley
ashawley

Reputation: 4283

Your question has two parts. It is possible to automatically generate classes in ScalaCheck with Gen.resultOf. This was previously asked in scalacheck case class random data generator. It may be possible to have scalacheck-shapeless do it with even less boilerplate.

The second part of your question is about mutating fields in immutable objects. For the most part, just using copy constructor provided by case class is sufficient. A library like Monocle can help, but I've never had to use it.

import org.scalacheck.Arbitrary
import org.scalacheck.Prop
import org.scalacheck.Prop.AnyOperators // Adds ?= operator
import org.scalacheck.Properties
import org.scalacheck.Gen

object FooSpec extends Properties("Foo") {

  val genBaz: Gen[Baz] = Gen.resultOf(Baz)

  implicit val arbBaz = Arbitrary(genBaz)

  val genBar: Gen[Bar] = Gen.resultOf(Bar)

  implicit val arbBar = Arbitrary(genBar)

  val genFoo: Gen[Foo] = Gen.resultOf(Foo)

  implicit val arbFoo = Arbitrary(genFoo)

  val genCustomFoo: Gen[Foo] = {
    Arbitrary.arbitrary[Foo].map { foo =>
      foo.copy(bar = foo.bar.copy(baz = Baz("custom for testing")))
    }
  }

  property("arbFoo") = {
    Prop.forAll { foo: Foo  =>
      foo.bar.baz.thing != "custom for testing"
    }
  }

  property("genCustomFoo") = {
    Prop.forAll(genCustomFoo) { foo: Foo  =>
      foo.bar.baz.thing ?= "custom for testing"
    }
  }
}

Upvotes: 0

Valy Dia
Valy Dia

Reputation: 2851

Scalacheck does offer a const generator, that allow to define customized / constant strings:

import org.scalacheck._

  val fooGen: Gen[Foo] =
    for {
      baz <- Gen.const("custom for testing").map(Baz)
      price <- Gen.choose[Double](0, 5000)
      name <- Gen.alphaStr
      value <- Gen.choose(0, 100)
    } yield {
      val bar = Bar(baz, price)
      Foo(bar, name, value)
    }

Here is what we get when we run it:

scala> fooGen.sample
res6: Option[Foo] = Some(
  Foo(
    Bar(Baz("custom for testing"), 1854.3159675078969),
    "EegNcrrQyzuazqrkturrvsqylaauxausrkwtefowpbkptiuoHtdfJjoUImgddhsnjuzpoiVpjAtjzulkMonIrzmfxonBmtZS",
    64
  )
)

Update : As @Dima pointed out, a way to derive random values for all fields is to use [scalacheck-shapeless](https://github.com/alexarchambault/scalacheck-shapeless) and lenses for the customization, here is an example that uses Monocle:

  import org.scalacheck.{Arbitrary, Gen}
  import monocle.Lens
  import org.scalacheck.ScalacheckShapeless._

  implicitly[Arbitrary[Foo]]

  val lensBar = Lens[Foo, Bar](_.bar)(bar => _.copy(bar = bar))
  val lensBaz = Lens[Bar, Baz](_.baz)(baz => _.copy(baz = baz))
  val lensThing = Lens[Baz, String](_.thing)(thing => _.copy(thing = thing))

  val lens = (lensBar composeLens lensBaz composeLens lensThing).set("custom for testing")


  val fooGen: Gen[Foo] = Arbitrary.arbitrary[Foo].map(lens)


  println(fooGen.sample)
  // Display 
  // Some(Foo(Bar(Baz(custom for testing),1.2227226413326224E-91),〗❌䟤䉲㙯癏<,-2147483648))

Upvotes: 4

Dima
Dima

Reputation: 40510

I think, you are looking for scalaz lense.

It'll do what you want.

However, I gotta say, that using random data for unit testing seems like a horrible idea. How are you going to debug a failure that happens every now and again?

You should invest some time into setting up a deterministic set of constant test objects, that also resemble the actual production data, and then just use that in your tests.

Upvotes: 5

Related Questions