org.scalatest.prop

Generator

trait Generator[T] extends AnyRef

Base type for all Generators.

A Generator produces a stream of values of a particular type. This is usually a mix of randomly-created values (generally built using a Randomizer), as well as some well-known edge cases that tend to cause bugs in real-world code.

For example, consider an intGenerator that produces a sequence of Ints. Some of these will be taken from Randomizer.nextInt, which may result in any possible Int, so the values will be a very random mix of numbers. But it will also produce the known edge cases of Int:

The list of appropriate edge cases will vary from type to type, but they should be chosen so as to exercise the type broadly, and at extremes.

Creating Your Own Generators

Generator.intGenerator, and Generators for many other basic types, are already built into the system, so you can just use them. You can (and should) define Generators for your own types, as well.

In most cases, you do not need to write Generators from scratch -- Generators for most non-primitive types can be composed using for comprehensions, as described in the section Composing Your Own Generators in the documentation for the org.scalatest.prop pacakge. You can also often create them using the CommonGenerators.instancesOf method. You should only need to write a Generator from scratch for relatively primitive types, that aren't composed of other types.

If you decide that you do need to build a Generator from scratch, here is a rough outline of how to go about it.

First, look at the source code for some of the Generators in the Generator companion object. These follow a pretty standard pattern, that you will likely want to follow.

Size

Your Generator may optionally have a concept of size. What this means varies from type to type: for a String it might be the number of characters, whereas for a List it might be the number of elements. The test system will try using the Generator with a variety of sizes; you can control the maximum and minimum sizes via Configuration.

Decide whether the concept of size is relevant for your type. If it is relevant, you should mix the HavingSize or HavingLength trait into your Generator, and you'll want to take it into account in your next and shrink functions.

Randomization

The Generator should do all of its "random" data generation using the Randomizer instance passed in to it, and should return the next Randomizer with its results. Randomizer produces intentionally pseudo-random data: it looks reasonably random, but is actually entirely deterministic based on the seed used to initialize it. By consistently using Randomizer, the Generator can be re-run, producing the same values, when given an identically-seeded Randomizer. This can often make debugging much easier, since it allows you to reproduce your "random" failures.

So figure out how to create a pseudo-random value of your type using Randomizer. This will likely involve writing a function similar to the various nextT() functions inside of Randomizer itself.

next()

Using this randomization function, write a first draft of your Generator, filling in the next() method. This is the only required method, and should suffice to start playing with your Generator. Once this is working, you have a useful Generator.

Edges

The edges are the edge cases for this type. You may have as many or as few edge cases as seem appropriate, but most types involve at least a few. Edges are generally values that are particularly big/full, or particularly small/empty. The test system will prioritize applying the edge cases to the property, since they are assumed to be the values most likely to cause failures.

Figure out some appropriate edge cases for your type. Override initEdges() to return those, and enhance next() to produce them ahead of the random values. Identifying these will tend to make your property checks more effective, by catching these edge cases early.

Canonicals

Now figure out some canonical values for your type -- a few common, ordinary values that are frequently worth testing. These will be used when shrinking your type in higher-order Generators, so it is helpful to have some. Override the canonicals() method to return these.

Canonicals should always be in order from "smallest" to less-small, in the shrinking sense. This is not the same thing as starting with the lowest number, though! For example, the canonicals for Generator.byteGenerator are:

private val byteCanonicals: List[Byte] = List(0, 1, -1, 2, -2, 3, -3)

Zero is "smallest" -- the most-shrunk Byte.

Shrinking

Optionally but preferably, your Generator can have a concept of shrinking. This starts with a value that is known to cause the property evaluation to fail, and produces a list of smaller/simpler values, to see if those also fail. So for example, if a String of length 15 causes a failure, its Generator could try Strings of length 3, and then 1, and then 0, to see if those also cause failure.

You to not have to implement the Generator.shrink method, but it is helpful to do so when it makes sense; the test system will use that to produce smaller, easier-to-debug examples when something fails.

One important rule: the values returned from shrink must always be smaller than -- not equal to -- the values passed in. Otherwise, an infinite loop can result. Also, similar to Canonicals, the "smallest" values should be returned at the front of this Iterator, with less-small values later.

T

the type that this Generator produces

Self Type
Generator[T]
Source
Generator.scala
Linear Supertypes
AnyRef, Any
Ordering
  1. Alphabetic
  2. By inheritance
Inherited
  1. Generator
  2. AnyRef
  3. Any
  1. Hide All
  2. Show all
Learn more about member selection
Visibility
  1. Public
  2. All

Abstract Value Members

  1. abstract def next(szp: SizeParam, edges: List[T], rnd: Randomizer): (T, List[T], Randomizer)

    Produce the next value for this Generator.

    Produce the next value for this Generator.

    This is the heart and soul of Generator -- it is the one function that you are required to implement when creating a new one. It takes several fields describing the state of the current evaluation, and returns the next value to try, along with the new state.

    The state consists of three fields:

    • The size to generate, if that is meaningful for this Generator.
    • The remaining edge cases that need to be generated. In general, if this List is non-empty, you should simply return the next item on the List.
    • The current Randomizer. If you need to generate random information, use this to do so.

    This function returns a Tuple of three fields:

    • The next value of type T to try evaluating.
    • The remaining edges without the one that you are using. That is, if this function received a non-empty edges List, it should usually return the head as the next value, and the tail as the remainder after that.
    • If you used the passed-in Randomizer, return the one you got back from that function. (Note that all Randomizer functions return a new Randomizer. If you didn't use it, just return the one that was passed in.
    szp

    the size to generate, if that is relevant for this Generator

    edges

    the remaining edge cases to be tried

    rnd

    the Randomizer to use if you need "random" data

    returns

    a Tuple of the next value, the remaining edges, and the resulting Randomizer, as described above.

Concrete Value Members

  1. final def !=(arg0: AnyRef): Boolean

    Definition Classes
    AnyRef
  2. final def !=(arg0: Any): Boolean

    Definition Classes
    Any
  3. final def ##(): Int

    Definition Classes
    AnyRef → Any
  4. final def ==(arg0: AnyRef): Boolean

    Definition Classes
    AnyRef
  5. final def ==(arg0: Any): Boolean

    Definition Classes
    Any
  6. final def asInstanceOf[T0]: T0

    Definition Classes
    Any
  7. def canonicals(rnd: Randomizer): (Iterator[T], Randomizer)

    Some simple, "ordinary" values of type T.

    Some simple, "ordinary" values of type T.

    canonicals are used for certain higher-order functions, mainly during shrink. For example, when the system is trying to simplify a List[T], it will look for canonical values of T to try putting into that simpler list, to see if that still causes the property to fail.

    For example, a few of the common types provide these canonicals:

    • Int: 0, 1, -1, 2, -2, 3, -3
    • Char: the letters and digits
    • String: single-charactor Strings of the letter and digits

    You do not have to provide canonicals for a Generator. By default, this simply returns an empty Iterator.

    This function takes a Randomizer to use as a parameter, in case canonical generation for this type has a random element to it. If you use this Randomizer, return the next one. If you don't use it, just use the passed-in one.

    rnd

    a Randomizer to use if this function requires any random data

    returns

    the canonical values for this type (if any), and the next Randomizer

  8. def clone(): AnyRef

    Attributes
    protected[java.lang]
    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  9. final def eq(arg0: AnyRef): Boolean

    Definition Classes
    AnyRef
  10. def equals(arg0: Any): Boolean

    Definition Classes
    AnyRef → Any
  11. def filter(f: (T) ⇒ Boolean): Generator[T]

    Support for filtering in for comprehensions.

    Support for filtering in for comprehensions.

    This means the same thing is does for all standard Scala Monads: it applies a filter function to this Generator. If you use an if clause in a for comprehension, this is the function that will be called.

    It is closely related to Generator.withFilter, but is the older form.

    The default implementation of this has a safety check, such that if an enormous number of values (100000 by default) are rejected by the filter in a row, it aborts in order to prevent infinite loops. If this occurs, you should probably rewrite your generator to not use a filter.

    You generally should not need to override this.

    f

    the actual filter function, which takes a value of type T and says whether to include it or not

    returns

    a Generator that only produces values that pass this filter.

  12. def finalize(): Unit

    Attributes
    protected[java.lang]
    Definition Classes
    AnyRef
    Annotations
    @throws( classOf[java.lang.Throwable] )
  13. def flatMap[U](f: (T) ⇒ Generator[U]): Generator[U]

    The usual Monad function, to allow Generators to be composed together.

    The usual Monad function, to allow Generators to be composed together.

    This is primarily here to support the ability to create new Generators easily using for comprehensions. For example, say that you needed a Generator that produces a Tuple of an Int and a Float. You can write that easily:

    val tupleGen =
    for {
      a <- Generator.intGenerator
      b <- Generator.floatGenerator
    }
      yield (a, b)

    That is, flatMap takes a function that returns a Generator, and combines it with this Generator, to produce a new Generator. That function may make use of a value from this Generator (that is part of the standard contract of flatMap), but usually does not.

    U

    the type produced by the other Generator

    f

    a function that takes a value from this Generator, and returns another Generator

    returns

    a Generator that is this one and the other one, composed together

  14. final def getClass(): Class[_]

    Definition Classes
    AnyRef → Any
  15. def hashCode(): Int

    Definition Classes
    AnyRef → Any
  16. def initEdges(maxLength: PosZInt, rnd: Randomizer): (List[T], Randomizer)

    Prepare a list of edge-case values ("edges") for testing this type.

    Prepare a list of edge-case values ("edges") for testing this type.

    The contents of this list are entirely up to the Generator. It is allowed to be empty, but it is a good idea to think about whether there are appropriate edge cases for this type. (By default, this is empty, so you can get your Generator working first, and think about edge cases after that.)

    It is common, but not required, to randomize the order of the edge cases here. If so, you should use the Randomizer.shuffle function for this, so that the order is reproducible if something fails. If you don't use the Randomizer, just return it unchanged as part of the returned tuple.

    Note the maxLength parameter. This is the number of tests to be run in total. So the list of returned edges should be no longer than this.

    maxLength

    the maximum size of the returned List

    rnd

    the Randomizer that should be used if you want randomization of the edges

    returns

    a Tuple: the list of edges, and the next Randomizer

  17. final def isInstanceOf[T0]: Boolean

    Definition Classes
    Any
  18. def map[U](f: (T) ⇒ U): Generator[U]

    Given a function from types T to U, return a new Generator that produces values of type U.

    Given a function from types T to U, return a new Generator that produces values of type U.

    For example, say that you needed a Generator that only creates even Ints. We already have Generator.intGenerator, so one way to write this would be:

    val evenGen: Generator[Int] = Generator.intGenerator.map { i =>
    val mod = i % 2
    if ((mod == 1) || (mod == -1))
      // It is odd, so the one before it is even:
      i - 1
    else
      // Already even
      i
    }

    This often makes it much easier to create a new Generator, if you have an existing one you can base it on.

    U

    the type of Generator you want to create

    f

    a function from T to U

    returns

    a new Generator, based on this one and the given transformation function

  19. final def ne(arg0: AnyRef): Boolean

    Definition Classes
    AnyRef
  20. final def notify(): Unit

    Definition Classes
    AnyRef
  21. final def notifyAll(): Unit

    Definition Classes
    AnyRef
  22. def sample: T

    Fetch a generated value of type T.

    Fetch a generated value of type T.

    sample allows you to experiment with this Generator in a convenient, ad-hoc way. Each time you call it, it will create a new Randomizer and a random size, and then calls next to generate a value.

    You should not need to override this method; it is here to let you play with your Generator as you build it, and see what sort of values are actually coming out.

    returns

    a generated value of type T

  23. def samples(length: PosInt): List[T]

    Generate a number of values of type T.

    Generate a number of values of type T.

    This is essentially the same as sample, and all the same comments apply, but this will generate as many values as you ask for.

    length

    the number of values to generate

    returns

    a List of size length, of randomly-generated values

  24. def shrink(value: T, rnd: Randomizer): (Iterator[T], Randomizer)

    Given a value of type T, produce some smaller/simpler values if that makes sense.

    Given a value of type T, produce some smaller/simpler values if that makes sense.

    When a property evaluation fails, the test system tries to simplify the failing case, to make debugging easier. How this simplification works depends on the type of Generator. For example, if it is a Generator of Lists, it might try with shorter Lists; if it is a Generator of Strings, it might try with shorter Strings.

    The critical rule is that the values returned from shrink must be smaller/simpler than the passed-in value, and must not include the passed-in value. This is to ensure that the simplification process will always complete, and not go into an infinite loop.

    This function receives a Randomizer, in case there is a random element to the simplification process. If you use the Randomizer, you should return the next one; if not, simply return the passed-in one.

    You do not have to implement this function. If you do not, it will return an empty Iterator, and the test system will not try to simplify failing values of this type.

    This function returns a Tuple. The first element should be an Iterator that returns simplified values, and is empty when there are no more. The second element is the next Randomizer, as discussed above.

    value

    a value that failed property evaluation

    rnd

    a Randomizer to use, if you need random data for the shrinking process

    returns

    a Tuple of the shrunk values and the next Randomizer

  25. final def synchronized[T0](arg0: ⇒ T0): T0

    Definition Classes
    AnyRef
  26. def toString(): String

    Definition Classes
    AnyRef → Any
  27. final def wait(): Unit

    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  28. final def wait(arg0: Long, arg1: Int): Unit

    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  29. final def wait(arg0: Long): Unit

    Definition Classes
    AnyRef
    Annotations
    @throws( ... )
  30. def withFilter(f: (T) ⇒ Boolean): Generator[T]

    Support for filtering in for comprehensions.

    Support for filtering in for comprehensions.

    This means the same thing is does for all standard Scala Monads: it applies a filter function to this Generator. If you use an if clause in a for comprehension, this is the function that will be called.

    The default implementation of this has a safety check, such that if an enormous number of values (100000 by default) are rejected by the filter in a row, it aborts in order to prevent infinite loops. If this occurs, you should probably rewrite your generator to not use a filter.

    You generally should not need to override this.

    f

    the actual filter function, which takes a value of type T and says whether to include it or not

    returns

    a Generator that only produces values that pass this filter.

Inherited from AnyRef

Inherited from Any

Ungrouped