Built-in Generators
In the companion object of the Gen data type, there are tons of generators for various data types.
Primitive Types Generatorsâ
ZIO Test provides generators for primitive types such as Gen.int, Gen.string, Gen.boolean, Gen.float, Gen.double, Gen.bigInt, Gen.byte, Gen.bigdecimal, Gen.long, Gen.char, and Gen.short.
Let's create an Int generator:
import zio._
import zio.test._
val intGen: Gen[Any, Int] = Gen.int
Character Generatorsâ
In addition to Gen.char, ZIO Test offers a variety of specialized character generators:
Gen.alphaCharâ e.g.Z, z, A, t, o, e, K, E, y, NGen.alphaNumericCharâ e.g.b, O, X, B, 4, M, k, 9, a, pGen.asciiCharâ e.g., >, , , , 2, k, , ,Gen.unicodeCharâ e.g.īĻē, îŋ, ėˇ, ī¨, îŖ, 뎲, īš, įŽ, īŦ, áŖ)Gen.numericCharâ e.g.1, 0, 1, 5, 6, 9, 4, 4, 5, 2Gen.printableCharâ e.g.H, J, (, Q, n, g, 4, G, 9, lGen.whitespaceCharsâ e.g., , â, , , â, â, , á,Gen.hexCharâ e.g.3, F, b, 5, 9, e, 2, 8, b, eGen.hexCharLowerâ e.g.f, c, 4, 4, c, 2, 5, 4, f, 3Gen.hexCharUpperâ e.g.4, 8, 9, 8, C, 9, F, A, E, C
String Generatorsâ
Besides the primitive string generator, Gen.string, ZIO Test also provides the following specialized generators:
Gen.stringBoundedâ A generator of strings whose size falls within the specified bounds:
Gen.stringBounded(1, 5)(Gen.alphaChar)
.runCollectN(10)
.debug
// Sample Output: List(b, YJXzY, Aro, y, WMPbj, Abxt, kJep, LKN, kUtr, xJ)
Gen.stringNâ A generator of strings of fixed size:
Gen.stringN(5)(Gen.alphaChar)
.runCollectN(10)
.debug
// Sample Output: List(BuywQ, tXCEy, twZli, ffLwI, BPEbz, OKYTi, xeDJW, iDUVn, cuMCr, keQAA)
Gen.string1â A generator of strings of at least one character.Gen.alphaNumericStringâ A generator of alphanumeric characters.Gen.alphaNumericStringBoundedâ A generator of alphanumeric strings whose size falls within the specified bounds.Gen.iso_8859_1â A generator of strings that can be encoded in the ISO-8859-1 character set.Gen.asciiStringâ A generator of US-ASCII characters.
Generating Fixed Valuesâ
Gen.constâ A constant generator of the specified value.
Gen.const(true).runCollectN(5)
// Output: List(true, true, true, true, true)
Gen.constSampleâ A constant generator of the specified sample:
Gen.constSample(Sample.noShrink(false)).runCollectN(5)
// Output: List(true, true, true, true, true)
-
Gen.unitâ A constant generator of the unit value. -
Gen.throwableâ A generator of throwables.
Note that there is an empty generator called Gen.empty, which generates no values and returns nothing. We can think of that as a generator of empty stream, Gen(Stream.empty).
Generating from Fixed Valuesâ
Gen.elementsâ Constructs a non-deterministic generator that only generates randomly from the fixed values:
import java.time._
Gen.elements(
DayOfWeek.MONDAY,
DayOfWeek.TUESDAY,
DayOfWeek.WEDNESDAY,
DayOfWeek.THURSDAY,
DayOfWeek.FRIDAY,
DayOfWeek.SATURDAY,
DayOfWeek.SUNDAY
).runCollectN(3).debug
// Sample Output: List(WEDNESDAY, THURSDAY, SUNDAY)
Gen.fromIterableâ Constructs a deterministic generator that only generates the specified fixed values:
Gen.fromIterable(List("red", "green", "blue"))
.runCollectN(10)
.debug
Collection Generatorsâ
ZIO Test has generators for collection data types such as sets, lists, vectors, chunks, and maps. These data types share similar APIs. The following example illustrates how the generator of sets works:
// A sized generator of sets
Gen.setOf(Gen.alphaChar)
// Sample Output: Set(Y, M, c), Set(), Set(g, x, Q), Set(s), Set(f, J, b, R)
// A sized generator of non-empty sets
Gen.setOf1(Gen.alphaChar)
// Sample Output: Set(Y), Set(L, S), Set(i), Set(H), Set(r, Z, z)
// A generator of sets whose size falls within the specified bounds.
Gen.setOfBounded(1, 3)(Gen.alphaChar)
// Sample Output: Set(Q), Set(q, J), Set(V, t, h), Set(c), Set(X, O)
// A generator of sets of the specified size.
Gen.setOfN(2)(Gen.alphaChar)
// Sample Output: Set(J, u), Set(u, p), Set(i, m), Set(b, N), Set(B, Z)
Bounded Generatorâ
The Gen.bounded constructor is a generator whose size falls within the specified bounds:
Gen.bounded(2, 5)(Gen.stringN(_)(Gen.alphaChar))
.runCollectN(5)
Suspended Generatorâ
The Gen.suspend constructs a generator lazily. This is useful to avoid infinite recursion when creating generators that refer to themselves.
Unfold Generatorâ
The unfoldGen takes the initial state and depending on the previous state, it determines what will be the next generated value:
def unfoldGen[R, S, A](s: S)(f: S => Gen[R, (S, A)]): Gen[R, List[A]]
Assume we want to test the built-in scala stack (scala.collection.mutable.Stack). One way to do that is to create an acceptable series of push and pop commands, and then check that the stack doesn't throw any exception by executing these commands:
sealed trait Command
case object Pop extends Command
final case class Push(value: Char) extends Command
val genPop: Gen[Any, Command] = Gen.const(Pop)
def genPush: Gen[Any, Command] = Gen.alphaChar.map(Push)
val genCommands: Gen[Any, List[Command]] =
Gen.unfoldGen(0) { n =>
if (n <= 0)
genPush.map(command => (n + 1, command))
else
Gen.oneOf(
genPop.map(command => (n - 1, command)),
genPush.map(command => (n + 1, command))
)
}
We are now ready to test the generated list of commands:
import zio.test.{ test, _ }
test("unfoldGen") {
check(genCommands) { commands =>
val stack = scala.collection.mutable.Stack.empty[Int]
commands.foreach {
case Pop => stack.pop()
case Push(value) => stack.push(value)
}
assertCompletes
}
}
From a ZIO Effectâ
Gen.fromZIO
val gen: Gen[Any, Int] = Gen.fromZIO(Random.nextInt)
Gen.fromZIOSample
val gen: Gen[Any, Int] =
Gen.fromZIOSample(
Random.nextInt.map(Sample.shrinkIntegral(0))
)
From a Random Effectâ
Gen.fromRandomâ Constructs a generator from a function that uses randomness:
val gen: Gen[Any, Int] = Gen.fromRandom(_.nextInt)
Gen.fromRandomSampleâ Constructs a generator from a function that uses randomness to produce a sample:
val gen: Gen[Any, Int] =
Gen.fromRandomSample(
_.nextIntBounded(20).map(Sample.shrinkIntegral(0))
)
Uniform and Non-uniform Generatorsâ
-
Gen.uniformâ A generator of uniformly distributed doubles between [0, 1]. -
Gen.weightedâ A generator which chooses one of the given generators according to their weights. For example, the following generator will generate 90% true and 10% false values:
val trueFalse = Gen.weighted((Gen.const(true), 9), (Gen.const(false), 1))
trueFalse.runCollectN(10).debug
// Sample Output: List(false, false, false, false, false, false, false, false, true, false)
Gen.exponentialâ A generator of exponentially distributed doubles with mean1:
Gen.exponential.map(x => math.round(x * 100) / 100.0)
.runCollectN(10)
.debug
// Sample Output: List(0.22, 3.02, 1.96, 1.13, 0.81, 0.92, 1.7, 1.47, 1.55, 0.46)
Generating Date/Time Typesâ
| Date/Time Types | Generators |
|---|---|
java.time.DayOfWeek | Gen.dayOfWeek |
java.time.Month | Gen.month |
java.time.Year | Gen.year |
java.time.Instant | Gen.instant |
java.time.MonthDay | Gen.monthDay |
java.time.YearMonth | Gen.yearMonth |
java.time.ZoneId | Gen.zoneId |
java.time.ZoneOffset | Gen.zoneOffset |
java.time.ZonedDateTime | Gen.zonedDateTime |
java.time.OffsetTime | Gen.offsetTime |
java.time.OffsetDateTime | Gen.offsetDateTime |
java.time.Period | Gen.period |
java.time.LocalDate | Gen.localDate |
java.time.LocalDateTime | Gen.localDateTime |
java.time.LocalTime | Gen.localTime |
zio.duration.Duration | Gen.finiteDuration |
Function Generatorsâ
To test some properties, we need to generate functions. There are two types of function generators:
Gen.functionâ It takes a generator of typeBand produces a generator of functions fromAtoB:
def function[R, A, B](gen: Gen[R, B]): Gen[R, A => B]
Two A values will be considered to be equal, and thus will be guaranteed to generate the same B value, if they have the same
hashCode.
Gen.functionWithâ It takes a generator of typeBand also a hash function forAvalues, and produces a generator of functions fromAtoB:
def functionWith[R, A, B](gen: Gen[R, B])(hash: A => Int): Gen[R, A => B]
Two A values will be considered to be equal, and thus will be guaranteed to generate the same B value, if they have the same hash. This is useful when A does not implement hashCode in a way that is consistent with equality.
Accordingly, ZIO Test provides a variety of function generators for Function2, Function3, ..., and also the PartialFunction:
Gen.function2â Gen[R, C] => Gen[R, (A, B) => C]Gen.functionWith2â Gen[R, B] => ((A, B) => Int) => Gen[R, (A, B) => C]Gen.partialFunctionâ Gen[R, B] => Gen[R, PartialFunction[A, B]]Gen.partialFunctionWithâ Gen[R, B] => (A => Int) => Gen[R, PartialFunction[A, B]]
Let's write a test for ZIO.foldLeft operator. This operator has the following signature:
def foldLeft[R, E, S, A](in: => Iterable[A])(zero: => S)(f: (S, A) => ZIO[R, E, S]): ZIO[R, E, S]
We want to test the following property:
â (in, zero, f) => ZIO.foldLeft(in)(zero)(f) == ZIO(List.foldLeft(in)(zero)(f))
To test this property, we have an input of type (Int, Int) => Int. So we need a Function2 generator of integers:
val func2: Gen[Any, (Int, Int) => Int] = Gen.function2(Gen.int)
Now we can test this property:
import zio._
import zio.test.{test, _}
test("ZIO.foldLeft should have the same result with List.foldLeft") {
check(Gen.listOf(Gen.int), Gen.int, func2) { case (in, zero, f) =>
assertZIO(
ZIO.foldLeft(in)(zero)((s, a) => ZIO.attempt(f(s, a)))
)(Assertion.equalTo(
in.foldLeft(zero)((s, a) => f(s, a)))
)
}
}
Generating ZIO Valuesâ
- Successful effects (
Gen.successes):
val gen: Gen[Any, UIO[Int]] = Gen.successes(Gen.int(-10, 10))
- Failed effects (
Gen.failures):
val gen: Gen[Any, IO[String, Nothing]] = Gen.failures(Gen.string)
- Died effects (
Gen.died):
val gen: Gen[Any, UIO[Nothing]] = Gen.died(Gen.throwable)
- Cause values (
Gen.causes):
val causes: Gen[Any, Cause[String]] =
Gen.causes(Gen.string, Gen.throwable)
- Chained effects (
Gen.chined,Gen.chainedN): A generator of effects that are the result of chaining the specified effect with itself a random number of times.
Let's see some example of chained ZIO effects:
import zio._
val effect1 = ZIO(2).flatMap(x => ZIO(x * 2))
val effect2 = ZIO(1) *> ZIO(2)
By using Gen.chaned or Gen.chanedN generator, we can create generators of chained effects:
val chained : Gen[Any, ZIO[Any, Nothing, Int]] =
Gen.chained(Gen.successes(Gen.int))
val chainedN: Gen[Any, ZIO[Any, Nothing, Int]] =
Gen.chainedN(5)(Gen.successes(Gen.int))
- Concurrent effects (
Gen.concurrent): A generator of effects that are the result of applying concurrency combinators to the specified effect that are guaranteed not to change its value.
val random : Gen[Any, UIO[Int]] = Gen.successes(Gen.int).flatMap(Gen.concurrent)
val constant: Gen[Any, UIO[Int]] = Gen.concurrent(ZIO(3))
- Parallel effects (
Gen.parallel): A generator of effects that are the result of applying parallelism combinators to the specified effect that are guaranteed not to change its value.
val random: Gen[Any, UIO[String]] =
Gen.successes(Gen.string).flatMap(Gen.parallel)
val constant: Gen[Any, UIO[String]] =
Gen.parallel(ZIO("Hello"))
Generating Compound Typesâ
- tuples â We can combine generators using for-comprehension syntax and tuples:
val tuples: Gen[Any, (Int, Double)] =
for {
a <- Gen.int
b <- Gen.double
} yield (a, b)
Gen.oneOfâ It takes variable number of generators and select one of them:
sealed trait Color
case object Red extends Color
case object Blue extends Color
case object Green extends Color
Gen.oneOf(Gen.const(Red), Gen.const(Blue), Gen.const(Green))
// Sample Output: Green, Green, Red, Green, Red
Gen.optionâ A generator of optional values:
val intOptions: Gen[Any, Option[Int]] = Gen.option(Gen.int)
val someInts: Gen[Any, Option[Int]] = Gen.some(Gen.int)
val nons: Gen[Any, Option[Nothing]] = Gen.none
Gen.eitherâ A generator of either values:
val char: Gen[Any, Either[Char, Char]] =
Gen.either(Gen.numericChar, Gen.alphaChar)
Gen.collectAllâ Composes the specified generators to create a cartesian product of elements with the specified function:
val gen: ZIO[Any, Nothing, List[List[Int]]] =
Gen.collectAll(
List(
Gen.fromIterable(List(1, 2)),
Gen.fromIterable(List(3)),
Gen.fromIterable(List(4, 5))
)
).runCollect
// Output:
// List(
// List(1, 3, 4),
// List(1, 3, 5),
// List(2, 3, 4),
// List(2, 3, 5)
//)
Gen.concatAllâ Combines the specified deterministic generators to return a new deterministic generator that generates all the values generated by the specified generators:
val gen: ZIO[Any, Nothing, List[Int]] =
Gen.concatAll(
List(
Gen.fromIterable(List(1, 2)),
Gen.fromIterable(List(3)),
Gen.fromIterable(List(4, 5))
)
).runCollect
// Output: List(1, 2, 3, 4, 5)
Sized Generatorsâ
Gen.sizedâ A sized generator takes a function fromInttoGen[R, A]and creates a generator by applying a size to that function:
Gen.sized(Gen.int(0, _))
.runCollectN(10)
.provideCustomLayer(Sized.live(5))
.debug
// Sample Output: List(5, 4, 1, 2, 0, 4, 2, 0, 1, 2)
Gen.sizeâ A generator which accesses the size from the environment and generates that:
Gen.size
.runCollectN(5)
.provideCustomLayer(Sized.live(100))
.debug
// Output: List(100, 100, 100, 100, 100)
There are also three sized generators, named small, medium and large, that use an exponential distribution of size values:
Gen.smallâ The values generated will be strongly concentrated towards the lower end of the range but a few larger values will still be generated:
Gen.small(Gen.const(_))
.runCollectN(10)
.provideCustomLayer(Sized.live(1000))
.debug
// Output: List(6, 39, 73, 3, 57, 51, 40, 12, 110, 46)
Gen.mediumâ The majority of sizes will be towards the lower end of the range but some larger sizes will be generated as well:
Gen.medium(Gen.const(_))
.runCollectN(10)
.provideCustomLayer(Sized.live(1000))
.debug
// Output: List(93, 42, 58, 228, 42, 5, 12, 214, 106, 79)
Gen.largeâ Uses a uniform distribution of size values. A large number of larger sizes will be generated:
Gen.large(Gen.const(_))
.runCollectN(10)
.provideCustomLayer(Sized.live(1000))
.debug
// Output: List(797, 218, 596, 278, 301, 779, 165, 486, 695, 788)