Skip to content

Generators ​

Why generators are in a separate method ​

PHP attribute arguments must be constant expressions, so #[Given('x', Gen::int())] is not expressible. Instead name a method that returns array<string, ArbitraryInterface> keyed by parameter name. When the generators argument is omitted the runner falls back to a method named <testMethod>Generators.

In both adapters, generators and examples also accept a callable, which is how a provider gets reused between test classes: [Provider::class, 'method'], 'Provider::method', or an invokable object (new Provider()) — all valid attribute expressions on PHP 8.3. PHP 8.5 additionally allows an inline static function (): array { ... } and a first-class callable (Provider::method(...)). A string still resolves to a method on the test class first, so a local method named like a global function (range) keeps winning; the convention stays the default.

Declare generators (and examples) methods public static — or public if the body needs $this. Their only call site is this package's reflection, so static analysis sees them as unused: Rector's dead-code set deletes private ones (RemoveUnusedPrivateMethodRector). Public methods are safe, and Testo never treats a non-void-returning method as a test.

Generators ​

FactoryProducesShrinks
Gen::int()PHP_INT_MIN..PHP_INT_MAXtoward 0
Gen::intBetween($min, $max)[$min, $max]toward 0, clamped to range
Gen::intPositive()1..PHP_INT_MAXtoward 1
Gen::float()[0.0, 1.0)toward 0.0
Gen::floatBetween($min, $max)[$min, $max) — $max itself is never drawntoward the point of [$min, $max) nearest to 0.0 — never $max
Gen::bool()true / falsetrue -> false
Gen::string()Unicode, length 0..100 — half the characters ASCII printable, a tenth troublemakers (quotes, backslash, combining marks, zero-width joiner, right-to-left override, byte order mark, astral emoji), a tenth Latin-1/Latin Extended, a tenth the rest of the BMP, a fifth uniform over U+0001..U+10FFFFtoward '', then by length, then each character toward a
Gen::stringAscii()printable ASCII, length 0..100toward '', then by length, then each character toward a
Gen::stringOf($minLength, $maxLength)Unicode, bounded lengthtoward '', then by length, then each character toward a
Gen::stringFrom($alphabet, $minLength, $maxLength)characters from a fixed alphabet (multibyte OK)toward '', then by length, then each character toward the first alphabet character
Gen::bytes($minLength, $maxLength)raw byte strings (bytes 0..255)toward '', then by length, then each byte toward "\x00"
Gen::arrayOf($element, $minSize, $maxSize)lists of $element, size 0..100 by defaulttoward [], then by length, then each element
Gen::nonEmptyArrayOf($element, $maxSize)non-empty listsby length (never below 1), then each element
Gen::uniqueArrayOf($element, $minSize, $maxSize, $by)lists of pairwise-distinct elements — by === on the values, or with by: fn ($v) => $v->id by the int|string key the closure returns (any other key type is refused at generation time)like arrayOf, but element candidates colliding with another element (or key) are skipped
Gen::dictOf($key, $value, $minSize, $maxSize)maps with distinct keys from $key (int/string) and values from $value, size 0..100 by defaulttoward [], then by size, then each value (keys fixed)
Gen::record($shape)fixed-shape map ['field' => $arb, ...]each field via its arbitrary, key set fixed
Gen::elements($array)one value from an array (array form of oneOf); an ArbitraryInterface among them is refusedtoward earlier-listed distinct values
Gen::enum(SomeEnum::class)OneOfArbitrary over the enum's casestoward earlier-declared cases (declare simpler cases first)
Gen::constant($value)always $valuedoes not shrink
Gen::withEdgeCases($inner, ...$edgeCases)$inner with author-supplied edge values: one draw in five is one of themthrough the edge values first, in the listed order, then the inner tree
Gen::composite($body)a value built by a body that draws dependent values through a Draw — composite generatorsthe draws, earliest first; later draws re-drawn through the new range
Gen::randomEngine() / Gen::randomizer()a Random\Engine (or the Randomizer over it) drawn from the tape — shrinkable randomnesseach engine call is a draw#N of eight bytes shrinking toward "\0"
Gen::char()a single printable ASCII charactertoward a
Gen::uuid()RFC 4122 v4 UUID stringsdoes not shrink
Gen::datetime($min, $max)UTC DateTimeImmutable, timestamp in [$min, $max]toward the Unix epoch, clamped
Gen::floatSpecial()OneOfArbitrary over NAN, ±INF, -0.0 and the float representation edgestoward earlier-listed specials
Gen::intRange($min, $max)ordered pairs [lo, hi] with lo <= hiboth bounds shrink, order always holds
Gen::recursive($leaf, $wrap, $maxDepth)bounded recursive structures: $wrap lifts the previous level's arbitrarywithin the branch that generated the value
Gen::oneOf(...$values)one of the given values, not generators — an ArbitraryInterface among them is refused, because accepted it would become the generated value itself. Pick between generators with Gen::frequency()toward earlier-listed distinct values (put simpler values first)
Gen::nullable($inner)null or an $inner valueprefers null, then the inner tree
Gen::map($inner, $map)$inner transformed by $mapthrough the inner tree, re-applying $map
Gen::flatMap($inner, $flatMap)dependent generator returned by $flatMap($innerValue)source value first (dependent value regenerated), then the dependent tree
Gen::filter($inner, $predicate)$inner values satisfying $predicate (throws GenerationExhaustedException after 100 rejected draws — never yields an out-of-domain value)inner tree, pruning candidates that fail the predicate
Gen::tuple(...$elements)fixed-arity tuple, one value per elementeach position via its element, arity fixed
Gen::frequency($pairs)weighted choice over [weight, arbitrary] pairswithin the branch that generated the value
Gen::ipv4()IPv4 dotted-quad stringseach octet toward 0
Gen::ipv6()IPv6 addresses in the canonical RFC 5952 text form (lowercase, no leading zeros, longest zero run compressed to ::)each group toward 0, ending at ::
Gen::email()local@label.tld addressestoward the shortest local/label and first TLD
Gen::url()http(s)://host.tld[/path] URLstoward http://a.com
Gen::json($maxDepth)a JSON-encodable value (null/bool/int/float/string/list/object)within the generated structure
Gen::jsonString($maxDepth)the json_encode text of Gen::json()through the value's tree
Gen::regex($pattern) / Gen::stringMatching($pattern)strings matching a regex subset (compiled to combinators). Write the pattern without delimiters — [a-z]+, not /[a-z]+/; a delimited one is refused rather than compiled with the delimiters as literals. . and a negated class draw from printable ASCII (0x20..0x7E, never a newline)shorter/simpler matches (via the compiled trees)
Gen::subset($values, $minSize, $maxSize)subsets of a fixed ordered set — distinct members of $values in source order; duplicates in the source are rejectedsize first (toward the empty set), then each kept element toward earlier source positions — the minimal subset is a short prefix
Gen::commands($initialModel, $commandGenerators, $minLength, $maxLength)valid command sequences for stateful testing — see State machinedrops command blocks, then simplifies each command
Gen::rules($machine, $minLength, $maxLength)a RuleSequence over a rule-based machine class — see Rule-based machineslike commands: drops steps, then simplifies each step's arguments
Gen::swarm($arbitrary)swarm testing: each case may use only a non-empty subset of the wrapped choice generator's variants — see Swarm testinginside the subset the case came from, never widening back
Gen::forClass($class, $overrides, $skipInvalid, $maxDepth)instances built through a constructor, one generator per parameter: an override, then the @param psalm type (int<0, 100> beats a bare int), then the native type — see Generating from a classthrough each parameter's own tree
Gen::forParameters($reflectionFunction, $overrides)the same resolution over any function, method or closure, returned as array<string, ArbitraryInterface> in signature order; overrides may be partialper parameter, as above

Numeric generators (int*, float*) are boundary-biased: roughly one draw in five returns an in-range edge value (0, ±1, min, max for ints; 0.0 or min for floats), where bugs cluster, instead of a uniform one. Shrinking is unaffected. See Boundary bias.

Sized generators guarantee their minimum: uniqueArrayOf/dictOf (distinct elements/keys) and commands (applicable steps) may fall short of the drawn size when the value space runs out, but never fall below $min — an unreachable minimum throws GenerationExhaustedException rather than hand the property a too-small value.

All of the above live on Gen in rasuvaeff/property-testing-core — the same facade regardless of which adapter's #[Property]/forAll() calls it.