PropertyCheck
Rasuvaeff\PropertyTesting\PhpUnit\PropertyCheck
Class — Package: property-testing-phpunit — Source — Version: v1.0.0
Fluent builder mapping the engine's structured PropertyResult onto PHPUnit: a pass counts one assertion, every failing outcome surfaces as one AssertionFailedError carrying the engine failure as previous — the message is the engine's own (seed, original and shrunk arguments, shrink statistics included).
Environment parity with the Testo adapter: PROPERTY_RUNS overrides every run count, PROPERTY_SEED seeds unseeded properties (an explicit PhpUnit\seed() wins), PROPERTY_VERBOSE traces every run, and PROPERTY_DB enables the regression corpus — an explicit seed() disables replay, exactly like an attribute seed does under Testo. An Assume::that() discard is a discarded run inside the property, never a skipped PHPUnit test.
Constructor
__construct(
PHPUnit\Framework\TestCase $testCase,
string $id,
string $name,
array<array-key,mixed> $generators,
bool $idDerivedIndirectly,
)| Parameter | Type | Default | Description |
|---|---|---|---|
$testCase | PHPUnit\Framework\TestCase | required | |
$id | string | required | |
$name | string | required | |
$generators | array<array-key,mixed> | required | The PhpUnit\PropertyTesting::forAll() map as written; PhpUnit\check() rejects anything in it that is not an ArbitraryInterface keyed by a parameter name. |
$idDerivedIndirectly | bool | required |
Methods
currentId()
currentId(): stringThe id the corpus is keyed by, as currently resolved.
id()
id(string $id): selfNames the property, replacing the id derived from the calling method.
The id keys the regression corpus entry and every event, so it has to name the same property tomorrow. Derived from the caller it does — for a test method. From a closure it cannot: PHP 8.3 calls every closure of a class {closure}, so two properties in one file share a corpus key and overwrite each other's counterexample, and from 8.4 the name carries a line number that an edit above shifts, orphaning yesterday's entry. Neither throws — the corpus simply stops replaying the failure it exists to replay. Pest's it()/test() bodies are the common case.
The name given here is used verbatim, and it is also the property's display name, so one string identifies it in the corpus, in the events and in the printed output.
throws()
throws(non-empty-string $exceptionClass): selfThe exception class every trial must throw. A trial that throws it passes; one that throws anything else, or returns normally, fails and shrinks like any other counterexample.
This is the property-level replacement for expectException(), which cannot work inside a check() closure: the executor observes every throw before PHPUnit's own expectation mechanism does. A failed assertion is never the expected throw, and a class it is an instance of (\Exception, \Throwable) is refused here. A skip (markTestSkipped(), markTestIncomplete()) and an Assume::that() discard are still what they are — the environment's verdict about the run, never a pass earned by throwing.
runs()
runs(int $runs): selfNumber of successful checks to complete; discarded runs do not count.
seed()
seed(int $seed): selfPins the random phase's seed. A pinned seed also disables regression replay, so the pinned reproducibility wins over the corpus.
maxShrinks()
maxShrinks(int $maxShrinks): selfCap on accepted shrink steps; 0 disables shrinking.
maxDiscards()
maxDiscards(int $maxDiscards): selfMaximum discarded runs before the property gives up.
timeoutMs()
timeoutMs(int $timeoutMs): selfWall-clock deadline for a single run, in milliseconds.
budgetMs()
budgetMs(int $budgetMs): selfWall-clock budget for the whole random phase, in milliseconds.
shrink()
shrink(Runner\ShrinkMode $shrink): selfHow hard to minimise a counterexample: ShrinkMode::Full (the default), ShrinkMode::Off to report the input as generated, or ShrinkMode::Bounded together with shrinkBudgetMs().
shrinkBudgetMs()
shrinkBudgetMs(int $shrinkBudgetMs): selfWall-clock budget for the shrink descent, in milliseconds — the one knob here that costs determinism: how far the descent gets depends on how long the body takes, so the same seed can minimise differently on a fast and a slow machine. It answers "the descent hung", not "reproduce this exactly".
phases()
phases(list<\Runner\Phase> $phases): selfWhich stages this run performs, in run order — a subset trades coverage for time on purpose (replaying only the examples and the corpus turns a minutes-long suite into a seconds-long pull-request gate).
derandomize()
derandomize(bool $derandomize = true): selfDerives an unset seed from the property id instead of drawing it at random, so the same property on the same code always selects the same inputs. An explicit seed() still wins.
edgeCases()
edgeCases(Runner\EdgeCases $edgeCases): selfWhether the numeric generators keep biasing toward their boundary values (EdgeCases::Mixin, the default) or generate uniformly (EdgeCases::None).
Turn them off when the edges are what this property cannot use — a body discarding 0, a range end that violates a precondition — so the discard budget stops paying for one run in five.
exhaustive()
exhaustive(bool $exhaustive = true): selfWalk the whole parameter domain instead of sampling it, when every generator has a finite domain (Enumerable) and the product fits exhaustiveBudget(); otherwise the phase samples and a warning says why. runs() is ignored when it walks. PROPERTY_EXHAUSTIVE turns it on for the whole suite.
exhaustiveBudget()
exhaustiveBudget(int $exhaustiveBudget): selfThe largest domain exhaustive() walks; 10 000 by default.
flakyReplays()
flakyReplays(int $flakyReplays): selfRe-executions of the minimised counterexample after the descent — one that passes marks the counterexample flaky, with a Flaky: line in the failure. 2 by default; 0 disables the check.
searchRuns()
searchRuns(int $searchRuns): selfBodies the targeted search may execute after the random phase, for a body that calls Target::maximize()/minimize(): the best-scoring inputs are mutated one parameter at a time. 0 (the default) performs no search. PROPERTY_SEARCH_RUNS overrides it for the whole suite.
path()
path(string $path): selfReplays the shrink descent of an earlier failure, as reported by CounterExample::$path, instead of searching for it again. It needs the seed of the run that produced it — the steps mean nothing against another one — so check() refuses a path without a seed() or a PROPERTY_SEED.
auto()
auto(bool $auto = true): selfDerive a generator from the closure's signature for every parameter the PropertyTesting::forAll() map does not cover — the @param psalm type when the closure has a docblock (int<1, 300> beats a bare int), the native type otherwise, and an error naming the parameter for anything unreadable. The map becomes the overrides and may be partial — or empty: a fully-typed property needs no map at all.
Strictly opt-in, never the default: a bare int or float derives its full native domain, and only the property's author knows whether that is the intended one. There is deliberately no PROPERTY_AUTO environment variable — the environment dials the suite, while this changes what one property's arguments mean.
examples()
examples(list<list> $examples): selfFixed positional argument tuples run before the random phase. A failing example short-circuits and is reported unshrunk — it is already minimal.
listeners()
listeners(PropertyListener $listeners): selfObservers of the run's engine events, notified in the given order.
output()
output(resource $stdout, resource $stderr): selfRedirects the distribution report, discard warning and verbose trace — for tests of this adapter itself.
clock()
clock(Runner\Clock $clock): selfReplaces the runner's source of elapsed time — for tests of this adapter itself. The Testo adapter takes the same seam on its interceptor; without it the timeoutMs and budgetMs branches can only be reached by really waiting, which is not a test.
check()
check(callable $property): voidRuns the property. The closure's parameter names select the generators.