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, N
Gen.alphaNumericChar
â e.g.b, O, X, B, 4, M, k, 9, a, p
Gen.asciiChar
â e.g., >, , , , 2, k, , ,
Gen.unicodeChar
â e.g.īĻē, îŋ, ėˇ, ī¨, îŖ, 뎲, īš, įŽ, īŦ, áŖ)
Gen.numericChar
â e.g.1, 0, 1, 5, 6, 9, 4, 4, 5, 2
Gen.printableChar
â e.g.H, J, (, Q, n, g, 4, G, 9, l
Gen.whitespaceChars
â e.g., , â, , , â, â, , á,
Gen.hexChar
â e.g.3, F, b, 5, 9, e, 2, 8, b, e
Gen.hexCharLower
â e.g.f, c, 4, 4, c, 2, 5, 4, f, 3
Gen.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 typeB
and produces a generator of functions fromA
toB
:
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 typeB
and also a hash function forA
values, and produces a generator of functions fromA
toB
:
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 fromInt
toGen[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)