4

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.

ashawley
  • 3,849
  • 24
  • 38
PhD
  • 10,410
  • 12
  • 56
  • 102

3 Answers3

5

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.

Dima
  • 33,157
  • 5
  • 34
  • 54
  • The tests cover "various aspects" of the data. For example, I may not need/care about setting a member's address when all I'm doing is checking their "car info" for a particular test. The test is focusing on the "car aspect". Another one will focus on the address part i.e., the asserts will differ. If I have to setup giant objects for each test case, it's hard to know "what" is relevant to the test. Hence, the need for something that has the data thrown in for the "logic to work" but "don't care" about it. Only care about a "focused element" so to speak. – PhD May 10 '19 at 21:48
  • After checking out the link - it seems that it can get the mutability part, but no the random data generation part, right? Does that imply the need to use Scalacheck-shapeless + Lense to get what I want? – PhD May 10 '19 at 22:06
  • yes, lense is only about accessing deeply nested structures, nothing to do with data generation (which I still think, is totally wrong approach here, albeit, maybe, the easiest in the short run). If you only care about some fields, and not others, that (1) implies, that your data structures may be poorly designed, and (2) is solved with specifying default values fro constructor parameters. – Dima May 11 '19 at 11:10
  • I hear you. But I’m curious to know that have you always encountered/created data structures for which only contained all relevant fields needed to be set in tests? There’s a reason why things like `AutoFixture` exists in the .Net world. In the functional world you maybe right. Unfortunately these are Thrift objects. And you can only “feed them to your test” if you set everything - which maybe a different problem altogether. – PhD May 12 '19 at 19:40
  • There are plenty of things that exist with no reason. It's not an argument. Nothing wrong with setting up thrift objects fro the test – Dima May 12 '19 at 19:43
2

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))
Valy Dia
  • 2,568
  • 2
  • 10
  • 29
  • The issue with using the for-comprehension is when dealing with really large/deep objects. Manually providing values for each "field" may not scale for multiple tests. – PhD May 10 '19 at 20:19
  • I've updated the answer to include, a more relevant answer as @Dima suggested – Valy Dia May 11 '19 at 11:59
0

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"
    }
  }
}
ashawley
  • 3,849
  • 24
  • 38