Combining generators

Though it is not very hard to create a custom one, the absolute majority of generators is built by combining existing implementations. Gen offers a couple of methods that are very useful in such scenarios. The classic example is to use the map and flatMap methods to create a generator for a case class.

Let's demonstrate this with an example of playing cards:

sealed trait Rank
case class SymRank(s: Char) extends Rank {
override def toString: String = s.toString
}
case class NumRank(n: Int) extends Rank {
override def toString: String = n.toString
}
case class Card(suit: Char, rank: Rank) {
override def toString: String = s"$suit $rank"
}

First, we need some generators for suits and ranks which we can create by reusing existing oneOf and choose constructors:

val suits = Gen.oneOf('♡', '♢', '♤', '♧')
val numbers = Gen.choose(2, 10).map(NumRank)
val symbols = Gen.oneOf('A', 'K', 'Q', 'J').map(SymRank)

Now, we can combine our generators into the card generator using for comprehension:

val full: Gen[Card] = for {
suit <- suits
rank <- Gen.frequency((9, numbers), (4, symbols))
} yield Card(suit, rank)

We also use Gen.frequency in order to have a proper distribution of numbers and symbols produced by our combined generator.

It is easy to change this generator to only make cards for a pique pack by using the suchThat combinator:

val piquet: Gen[Card] = full.suchThat {
case Card(_, _: SymRank) => true
case Card(_, NumRank(n)) => n > 5
}

We can check that our generators produce trustworthy values by using the Prop.collect method:

scala> forAll(piquet) { card =>
| Prop.collect(card)(true)
| }.check
+ OK, passed 100 tests.
> Collected test data:
8% ♡ J
6% ♢ 7
6% ♡ 10
... (couple of lines more)
scala> forAll(full) { card =>
| Prop.collect(card)(true)
| }.check
+ OK, passed 100 tests.
> Collected test data:
6% ♡ 3
5% ♢ 3
... (a lot more lines)

Of course, it is also possible to generate a handfull of cards from the deck using one of the container generator methods:

val handOfCards: Gen[List[Card]] = Gen.listOfN(6, piquet)

And use it as before:

scala> forAll(handOfCards) { hand: Seq[Card] =>
| Prop.collect(hand.mkString(","))(true)
| }.check
! Gave up after only 58 passed tests. 501 tests were discarded.
> Collected test data:
2% ♤ 8,♤ 10,♤ 8,♤ 7,♡ Q,♢ 8

Oh, we have duplicate cards in our hand. It turns out that we need to use a more general form of the container generator, which takes both the type of the container and the type of the element as type parameters:

val handOfCards = Gen.containerOfN[Set, Card](6, piquet)
scala> forAll(handOfCards) { hand =>
| Prop.collect(hand.mkString(","))(true)
| }.check
! Gave up after only 75 passed tests. 501 tests were discarded.
> Collected test data:
1% ♡ A,♤ J,♡ K,♢ 6,♧ K,♧ A
1% ♤ 9,♧ A,♧ 8,♧ 9

That is better, but now it seems that the duplicate elements have just disappeared so that we still don't have an expected behavior. Moreover, another issue is obvious—a lot of tests are discarded. This happens because our piquet generator is defined in terms of filtering the output of the more general full generator. ScalaCheck notices that there are too many tests which do not qualify as a valid input and gives up earlier.

Let's fix our piquet generator and an issue with missing cards. For the first one, we will use the same approach as we've used for the full generator. We'll just change the number used for the rank:

val piquetNumbers = Gen.choose(6, 10).map(NumRank)

val piquet: Gen[Card] = for {
suit <- suits
rank <- Gen.frequency((5, piquetNumbers), (4, symbols))
} yield Card(suit, rank)

Please note how the frequency changed in respect to the changed set of possible values.

To fix the second issue, we will repeatedly generate the set of cards until it has an expected size using retryUntil combinator:

val handOfCards = Gen.containerOfN[Set, Card](6, piquet).retryUntil(_.size == 6)

scala> forAll(handOfCards) { hand =>
| Prop.collect(hand.mkString(","))(true)
| }.check
+ OK, passed 100 tests.
> Collected test data:
1% ♤ 9,♢ 9,♧ 9,♢ Q,♧ J,♤ 10
...

Now, our hands are generated as expected.

Of course, there are even more useful combinator methods, which can be used to create other sophisticated generators. Please refer to the documentation (https://github.com/rickynils/scalacheck/blob/master/doc/UserGuide.md) or the source code for further details.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.159.178