9

Suppose I've got a few case classes, e.g.:

case class C(c1: Int, c2: Double, c3: Option[String])
case class B(b: Int, cs: Seq[C])
case class A(a: String, bs: Seq[B]) 

Now I would like to generate a few instances of A with random values for tests.

I am looking for a generic way to do that. I can probably do it with runtime reflection but I prefer a compile-time solution.

def randomInstance[A](a: A): A = ???

How can I do it ? Can it be done with shapeless ?

Michael
  • 41,026
  • 70
  • 193
  • 341
  • `scalacheck` has [`Arbitrary`](https://www.programcreek.com/scala/org.scalacheck.Arbitrary), which should be useful. – erip Jun 03 '18 at 14:26

3 Answers3

11

The easiest way for you to do that would be using ScalaCheck. You do so by defining a Gen[A] for your instances:

import org.scalacheck.Gen

final case class C(c1: Int, c2: Double, c3: Option[String])
object C {
  val cGen: Gen[C] = for {
    c1 <- Gen.posNum[Int]
    c2 <- Gen.posNum[Double]
    c3 <- Gen.option(Gen.oneOf("foo", "bar", "hello"))
  } yield C(c1, c2, c3)
}

And you consume it:

object F {
  def main(args: Array[String]): Unit = {
    val randomC: C = C.cGen.sample.get
  }
}

On top of that, you can add scalacheck-shapeless which generates the Gen[A] for you, with completely random values (where you have no control over them).

You may also want to look into random-data-generator (thanks @Gabriele Petronella), which simplifies things even further. From the docs:

import com.danielasfregola.randomdatagenerator.RandomDataGenerator

object MyApp extends RandomDataGenerator {

  case class Example(text: String, n: Int)

  val example: Example = random[Example]
  // Example(ਈ䈦㈾钜㔪旅ꪔ墛炝푰⡨䌆ᵅ퍧咪, 73967257)
}

This is also especially helpful in property based testing.

Yuval Itzchakov
  • 146,575
  • 32
  • 257
  • 321
5

We've just moved away from scalacheck-shapeless and use Scala/Java reflection instead.

The main reasons are (1) scalacheck-shapeless uses Macros (slow compilation), (2) the API is a bit more verbose than my liking, and (3) the generated values are way too wild (e.g. generating strings with Japanese characters).

However, setting it up is a bit more involved. Here is a full working code that you can copy into your codebase:

import scala.reflect.api
import scala.reflect.api.{TypeCreator, Universe}
import scala.reflect.runtime.universe._

object Maker {
  val mirror = runtimeMirror(getClass.getClassLoader)

  var makerRunNumber = 1

  def apply[T: TypeTag]: T = {
    val method = typeOf[T].companion.decl(TermName("apply")).asMethod
    val params = method.paramLists.head
    val args = params.map { param =>
      makerRunNumber += 1
      param.info match {
        case t if t <:< typeOf[Enumeration#Value] => chooseEnumValue(convert(t).asInstanceOf[TypeTag[_ <: Enumeration]])
        case t if t =:= typeOf[Int] => makerRunNumber
        case t if t =:= typeOf[Long] => makerRunNumber
        case t if t =:= typeOf[Date] => new Date(Time.now.inMillis)
        case t if t <:< typeOf[Option[_]] => None
        case t if t =:= typeOf[String] && param.name.decodedName.toString.toLowerCase.contains("email") => s"random-$arbitrary@give.asia"
        case t if t =:= typeOf[String] => s"arbitrary-$makerRunNumber"
        case t if t =:= typeOf[Boolean] => false
        case t if t <:< typeOf[Seq[_]] => List.empty
        case t if t <:< typeOf[Map[_, _]] => Map.empty
        // Add more special cases here.
        case t if isCaseClass(t) => apply(convert(t))
        case t => throw new Exception(s"Maker doesn't support generating $t")
      }
    }

    val obj = mirror.reflectModule(typeOf[T].typeSymbol.companion.asModule).instance
    mirror.reflect(obj).reflectMethod(method)(args:_*).asInstanceOf[T]
  }

  def chooseEnumValue[E <: Enumeration: TypeTag]: E#Value = {
    val parentType = typeOf[E].asInstanceOf[TypeRef].pre
    val valuesMethod = parentType.baseType(typeOf[Enumeration].typeSymbol).decl(TermName("values")).asMethod
    val obj = mirror.reflectModule(parentType.termSymbol.asModule).instance

    mirror.reflect(obj).reflectMethod(valuesMethod)().asInstanceOf[E#ValueSet].head
  }

  def convert(tpe: Type): TypeTag[_] = {
    TypeTag.apply(
      runtimeMirror(getClass.getClassLoader),
      new TypeCreator {
        override def apply[U <: Universe with Singleton](m: api.Mirror[U]) = {
          tpe.asInstanceOf[U # Type]
        }
      }
    )
  }

  def isCaseClass(t: Type) = {
    t.companion.decls.exists(_.name.decodedName.toString == "apply") &&
      t.decls.exists(_.name.decodedName.toString == "copy")
  }
}

And, when you want to use it, you can call:

val user = Maker[User]
val user2 = Maker[User].copy(email = "someemail@email.com")

The code above generates arbitrary and unique values. They aren't exactly randomised. It's best for using in tests.

Read our full blog post here: https://give.engineering/2018/08/24/instantiate-case-class-with-arbitrary-value.html

Tanin
  • 1,853
  • 1
  • 15
  • 20
4

We've started using Magnolia, which provides a faster type class derivation compared to shapeless for derivation of Arbitrary instances.

Here is the library to use, and here is an example (docs):

case class Inner(int: Int, str: String)
case class Outer(inner: Inner)

// ScalaCheck Arbitrary
import magnolify.scalacheck.auto._
import org.scalacheck._ // implicit instances for Arbitrary[Int], etc.

val arb: Arbitrary[Outer] = implicitly[Arbitrary[Outer]]
arb.arbitrary.sample
// = Some(Outer(Inter(12345, abcde)))
Anish
  • 51
  • 1
  • 5