How to get scalacheck to work on class with a Seq?

156 Views Asked by At

I have a case class that I am trying to test via ScalaCheck. The case class contains other classes.

Here are the classes:

case class Shop(name: String = "", colors: Seq[Color] = Nil)
case class Color(colorName: String = "", shades: Seq[Shade] = Nil)
case class Shade(shadeName: String, value: Int)

I have generators for each one

implicit def shopGen: Gen[Shop] = 
  for {
    name <- Gen.alphaStr.suchThat(_.length > 0)
    colors <- Gen.listOf(colorsGen)
  } yield Shop(name, colors)

implicit def colorsGen: Gen[Color] =
  for {
   colorName <- Gen.alphaStr.suchThat(_.length > 0)
   shades <- Gen.listOf(shadesGen)
  } yield Color(colorName, shades)

implicit def shadesGen: Gen[Shade] = 
  for {
    shadeName <- Gen.alphaStr.suchThat(_.length > 0) //**Note this**
    value <- Gen.choose(1, Int.MaxValue)
  } yield Shade(shadeName, value)

When I write my test and simply do the below:

  property("Shops must encode/decode to/from JSON") {
     "test" mustBe "test   
  }

I get an error and the test hangs and stops after 51 tries. The error I get is Gave up after 1 successful property evaluation. 51 evaluations were discarded.

If I remove Gen.alphaStr.suchThat(_.length > 0) from shadesGen and just replace it with Gen.alphaStr then it works.

Question

  1. Why does having Gen.alphaStr work for shadesGen, however, Gen.alphaStr.suchThat(_.length > 0) does not?
  2. Also when I run test multiple times (with Gen.alphaStr) some pass while some don't. Why is this?
1

There are 1 best solutions below

0
On

You probably see this behavior because of the way listOf is implemented. Inside it is based on buildableOf which is in turn based on buildableOfN which has following comment:

... If the given generator fails generating a value, the complete container generator will also fail.

Your data structure is essentially a list of lists so even one bad generation will curse the whole data-structure to be discarded. And obviously most of the failures happens at the bottom level. That's why removing the filter for shadeName helps. So to make it work you should generate more valid strings. You may change Gen.alphaStr to some custom-made generator based on nonEmptyListOf such as:

def nonemptyAlphaStr:Gen[String] = Gen.nonEmptyListOf(alphaChar).map(_.mkString)

Another simple way to work this around is to use retryUntil instead of suchThat such as in:

implicit def shadesGen: Gen[Shade] =
  for {
    //shadeName <- Gen.alphaStr.suchThat(_.length > 0) //**Note this**
    shadeName <- Gen.alphaStr.retryUntil(_.length > 0)
    value <- Gen.choose(1, Int.MaxValue)
  } yield Shade(shadeName, value)