Search code examples
scalascalatestscalacheck

Scala Check/Scala Test: Compose Generators


Is there a way I can compose generators in scala test/scala check?

For example, here is an example test case I'd like to write:

"The classifier" when {
  "given a string containing a state" should {
    "classify it as a state" in {
      val states = Seq(
        "Alabama", "Alaska", "Arizona", "Arkansas", "California", "Colorado", "Connecticut", "Delaware",
        "Florida", "Georgia", "Hawaii", "Idaho", "Illinois", "Indiana", "Iowa", "Kansas", "Kentucky",
        "Louisiana", "Maine", "Maryland", "Massachusetts", "Michigan", "Minnesota", "Mississippi",
        "Missouri", "Montana", "Nebraska", "Nevada", "New Hampshire", "New Jersey", "New Mexico",
        "New York", "North Carolina", "North Dakota", "Ohio", "Oklahoma", "Oregon", "Pennsylvania",
        "Rhode Island", "South Carolina", "South Dakota", "Tennessee", "Texas", "Utah", "Vermont",
        "Virginia", "Washington", "West Virginia", "Wisconsin", "Wyoming"
      )

      val inputData = for {
        zip <- Gen.const("10001")
        name <- Gen.oneOf(
          Gen.oneOf(states) + "HARRINGTON, JOHN",
          "HARRINGTON, JOHN " + Gen.oneOf(states),
          "HARRINGTON, " + Gen.oneOf(states) + " MD,JOHN"
        )
      } yield (zip, name)

      forAll (inputData) { case (zip: String, name: String) =>
        Clasifier.classify(zip, name) shouldBe Classification.STATE
      }
    }
  }
}

Please note the name generator in the for comprehension to derive the inputData val.

How can I achieve something like this?

Update: I've gotten this to work, but not sure if I'm doing the right thing here.

      val inputData = for {
        zip <- Gen.const("10001")
        name <- Gen.oneOf(
          s"${Gen.oneOf(states).sample.get} HARRINGTON, JOHN",
          s"HARRINGTON, JOHN ${Gen.oneOf(states).sample.get}",
          s"HARRINGTON, ${Gen.oneOf(states).sample.get} MD,JOHN"
        )
      } yield (zip, name)

The failure (expected failure) message isn't very helpful with what I'm doing:

TestFailedException was thrown during property evaluation.
  Message: STATE was not equal to INDIVIDUAL
  Location: (Classifier$Test.scala:142)
  Occurred when passed generated values (
    arg0 = (,) // 12 shrinks
  )

PS: As asked for in the comment, here's what I'm expecting the input data to look like:

"Alabama Harringgon, John",
"Harriongton, Alabama John",
"Harrington, John Alabama",
"Maryland Harrington, John",
"Harrington, Maryland John",
"Harrington, John Maryland",
etc.

Solution

  • If you want to compose your generators you can just include the state in the for comprehension like so:

    val inputData = for {
        zip <- Gen.const("10001")
        state <- Gen.oneOf(states)
        name <- Gen.oneOf(
          state + "HARRINGTON, JOHN",
          "HARRINGTON, JOHN " + state,
          "HARRINGTON, " + state + " MD,JOHN"
        )
      } yield (zip, name)
    

    Doing sample.get as you do in your updated example will throw an exception if the result of sample is None which won't be what you want.

    The reason you are getting the unhelpful failure message is due to shrinking where scalacheck will attempt to reduce the values to the minimum values that fail, in this case when both values are empty strings. In most cases I have found that this is not the behaviour you want and you can stop it from shrinking values by including an implicit like so:

    implicit val noShrinkString: Shrink[String] = Shrink.shrinkAny
    implicit def noShrinkList[A]: Shrink[List[A]] = Shrink.shrinkAny
    

    The first line stops strings from shrinking and the second line stops lists from shrinking by assigning them the same shrink instance of type Any which is to not shrink at all.

    The answer is a little late and I'm sure you have resolved your issue by now but hopefully this will help some else in a similar situation