PHPackages                             rasuvaeff/property-testing-core - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Testing &amp; Quality](/categories/testing)
4. /
5. rasuvaeff/property-testing-core

ActiveLibrary[Testing &amp; Quality](/categories/testing)

rasuvaeff/property-testing-core
===============================

Framework-agnostic property-based testing engine: generators, shrinking, runner, regression corpus, events

v0.1.0(yesterday)0121↑2602.5%[1 issues](https://github.com/rasuvaeff/property-testing-core/issues)2BSD-3-ClausePHPPHP 8.3 - 8.5CI passing

Since Aug 9Pushed todayCompare

[ Source](https://github.com/rasuvaeff/property-testing-core)[ Packagist](https://packagist.org/packages/rasuvaeff/property-testing-core)[ Docs](https://github.com/rasuvaeff/property-testing-core)[ RSS](/packages/rasuvaeff-property-testing-core/feed)WikiDiscussions master Synced today

READMEChangelog (1)Dependencies (10)Versions (2)Used By (2)

rasuvaeff/property-testing-core
===============================

[](#rasuvaeffproperty-testing-core)

[![Latest Stable Version](https://camo.githubusercontent.com/b50779d8f92247a2c508930c0924a549b5566489894f16047b5e050a2341dbec/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f70726f70657274792d74657374696e672d636f72652f76)](https://packagist.org/packages/rasuvaeff/property-testing-core)[![Total Downloads](https://camo.githubusercontent.com/4e07b24115d582b2f111b193bcdb38d243cedffb4498b851573284d2497e4e8c/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f70726f70657274792d74657374696e672d636f72652f646f776e6c6f616473)](https://packagist.org/packages/rasuvaeff/property-testing-core)[![Build](https://github.com/rasuvaeff/property-testing-core/actions/workflows/build.yml/badge.svg)](https://github.com/rasuvaeff/property-testing-core/actions/workflows/build.yml)[![Static analysis](https://github.com/rasuvaeff/property-testing-core/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/rasuvaeff/property-testing-core/actions/workflows/static-analysis.yml)[![Psalm level](https://camo.githubusercontent.com/68f7f31799f2b93c710b14ba3877072e7fe07ec9d7cee3fdf67e14beab3e1b6f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7073616c6d2d6c6576656c5f312d626c75652e737667)](https://github.com/rasuvaeff/property-testing-core/actions/workflows/static-analysis.yml)[![PHP](https://camo.githubusercontent.com/2faca6b813e2355339bf6d008fbe3ad4c7ffe6e6b82d25e892d19258857c0d43/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f7261737576616566662f70726f70657274792d74657374696e672d636f72652f706870)](https://packagist.org/packages/rasuvaeff/property-testing-core)[![License](https://camo.githubusercontent.com/6cb285b57819f8de0acfb34923298f4f569f962544e8fe35331da2d163f4e485/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4253442d2d332d2d436c617573652d626c75652e737667)](LICENSE.md)

[Русская версия](README.ru.md)

Framework-agnostic property-based testing **engine** for PHP 8.3+: generators with integrated shrinking, a structured property runner, a regression corpus, lifecycle events, and stateful/model-based testing — with no dependency on any test framework. Generate hundreds of random inputs, find the failing one, and shrink it to a minimal counterexample you can actually read.

> Using an AI coding assistant? [llms.txt](llms.txt) contains a compact API reference you can share with the model.

Part of the property-testing family
-----------------------------------

[](#part-of-the-property-testing-family)

PackageUse it when**`rasuvaeff/property-testing-core`** (this package)You drive the engine yourself: a custom harness, CI guard, CLI checker, or another framework adapter[`rasuvaeff/property-testing-testo`](https://github.com/rasuvaeff/property-testing-testo)You test with [Testo](https://github.com/php-testo/testo) — drop-in replacement for the frozen `rasuvaeff/property-testing` with the same `#[Property]` attribute[`rasuvaeff/property-testing-phpunit`](https://github.com/rasuvaeff/property-testing-phpunit)You test with PHPUnit — a `PropertyTesting` trait with a fluent `forAll()->check()` API> **Note:** this package `conflict`s with the frozen `rasuvaeff/property-testing`(2.x) — both ship classes in the `Rasuvaeff\PropertyTesting` namespace, so Composer refuses to install them together. Migrating from 2.x? Swap the dev dependency for the adapter matching your framework; your imports stay as they are. [MIGRATION.md](MIGRATION.md) is the full guide: two Composer commands and no PHP edits for Testo projects, plus the custom-harness and PHPUnit paths.

Requirements
------------

[](#requirements)

- PHP 8.3+
- `ext-mbstring`
- `ext-random`

Installation
------------

[](#installation)

```
composer require --dev rasuvaeff/property-testing-core
```

The engine has no test-framework dependency: you hand it a property definition and an executor, it hands you back a structured result. It never reads environment variables, never prints, never exits, and never throws to report a property outcome.

Usage
-----

[](#usage)

Build a `PropertyDefinition` (generators keyed by parameter name plus a `PropertyConfig`), execute the body through a `TrialExecutor`(`CallableTrialExecutor` adapts a plain closure), and inspect the `PropertyResult` the `PropertyRunner` returns:

```
use Rasuvaeff\PropertyTesting\Gen;
use Rasuvaeff\PropertyTesting\Runner\CallableTrialExecutor;
use Rasuvaeff\PropertyTesting\Runner\Falsified;
use Rasuvaeff\PropertyTesting\Runner\PropertyConfig;
use Rasuvaeff\PropertyTesting\Runner\PropertyDefinition;
use Rasuvaeff\PropertyTesting\Runner\PropertyRunner;

$definition = new PropertyDefinition(
    id: 'demo::everyIntStaysBelowHundred',
    name: 'everyIntStaysBelowHundred',
    generators: ['value' => Gen::intBetween(0, 10_000)],
    parameterNames: ['value'],
    config: new PropertyConfig(runs: 200, seed: 42),
);

$result = (new PropertyRunner())->run($definition, new CallableTrialExecutor(
    static function (int $value): void {
        if ($value >= 100) {
            throw new RuntimeException(sprintf('%d is not below 100', $value));
        }
    },
));

if ($result instanceof Falsified) {
    $example = $result->counterExample();
    // $example->seed, $example->originalArguments, $example->shrunkArguments, ...
    fwrite(STDERR, $result->failure()->getMessage());
}
```

The failure message renders the counterexample:

```
Property falsified after 0 successful run(s); seed=42
  Original: value=54
  Shrunk:   value=100 (3 shrink step(s), 11 trial(s))
  Changed:  value=54 -> 100
  Failure:  100 is not below 100

```

The `Changed:` line diffs the original against the shrunk counterexample — arguments the shrinker left untouched are omitted. `trial(s)` counts every candidate the shrinker ran (accepted and rejected); `shrink step(s)` counts only the accepted ones. Reproduce the exact run by pinning the reported seed in `PropertyConfig`.

See [`examples/standalone_runner.php`](examples/standalone_runner.php) for the full runnable script.

### The executor seam

[](#the-executor-seam)

`TrialExecutor` is the boundary between the engine and whatever executes the property body. Each `execute($arguments)` call returns a `TrialOutcome` — `passed()`, `failed($throwable)`, or `discarded()`:

- `CallableTrialExecutor` — the standalone executor: a normal return passes, `Assume::that()` discards, any other throwable fails the trial.
- Framework adapters implement their own (Testo maps a `TestResult`, PHPUnit maps assertion exceptions) — the run/shrink loop never learns about framework types.

### Structured results

[](#structured-results)

`PropertyRunner::run()` returns one of a closed `PropertyResult` hierarchy — impossible data combinations are not representable, and every failing outcome carries the engine's own exception type with the established message format:

ResultMeaningCarries`Passed`Every check completed, every coverage requirement held`RunStatistics``Falsified`A random run failed; the counterexample is shrunk`PropertyViolationException` → `CounterExample``GaveUp`Discard budget exhausted before `runs` checks`GaveUpException`, `RunStatistics``CoverageFailed`Every run passed but a `Classify::cover()` requirement was missed`CoverageViolationException`, `RunStatistics``DeadlineExceeded`A single run overran `timeoutMs``DeadlineExceededException``TimeBudgetExceeded`The random phase overran `budgetMs``TimeBudgetExceededException`, `RunStatistics``GenerationFailed`A generator could not produce a valid value`GenerationExhausted``ExampleFailed`An explicit example failed (examples run first, unshrunk)`ExampleViolationException``RegressionFailed`A recorded corpus entry still fails`RegressionViolationException`Configuration errors (`runs < 1`, a missing generator, mismatched parameter names) remain exceptions — they are programmer errors, not verdicts about the property.

`RunStatistics` exposes the raw phase counters (attempts, discards, checks, per-label classification counts) so a reporter can print a distribution table or a discard warning — the engine itself never formats framework output.

Serialization: every result survives native `serialize()` when captured stack traces carry no argument values (`zend.exception_ignore_args=1`); the portable machine format is `CounterExample::toArray()` / `toJson()`.

### Generators

[](#generators)

All factories live on the `Gen` facade; each returns an implementation of `ArbitraryInterface` whose `generate(Random)` yields a `Shrinkable` — the value plus a lazy tree of smaller candidates, so transformed generators shrink through their source domain.

FactoryProducesShrinks`Gen::int()``IntArbitrary`, `PHP_INT_MIN..PHP_INT_MAX`toward `0``Gen::intBetween($min, $max)``IntArbitrary`, `[$min, $max]`toward `0`, clamped to range`Gen::intPositive()``IntArbitrary`, `1..PHP_INT_MAX`toward `1``Gen::float()``FloatArbitrary`, `[0.0, 1.0)`toward `0.0``Gen::floatBetween($min, $max)``FloatArbitrary`, `[$min, $max]`toward `0.0`, clamped to range`Gen::bool()``BoolArbitrary`, `true` / `false``true` -&gt; `false``Gen::string()``StringArbitrary`, Unicode, length 0..100toward `''`, then by length, then each character toward `a``Gen::stringAscii()``StringArbitrary`, printable ASCII, length 0..100toward `''`, then by length, then each character toward `a``Gen::stringOf($min, $max)``StringArbitrary`, Unicode, bounded lengthtoward `''`, then by length, then each character toward `a``Gen::stringFrom($alphabet, $min, $max)``CharsetStringArbitrary`, characters from a fixed alphabet (multibyte OK)toward `''`, then by length, then each character toward the first alphabet character`Gen::bytes($min, $max)``BytesArbitrary`, raw byte strings (bytes 0..255)toward `''`, then by length, then each byte toward `"\x00"``Gen::arrayOf($element, $min, $max)``ArrayArbitrary`, lists of `$element`, size 0..100 by defaulttoward `[]`, then by length, then each element`Gen::nonEmptyArrayOf($element, $max)``ArrayArbitrary`, non-empty listsby length (never below 1), then each element`Gen::uniqueArrayOf($element, $min, $max)``UniqueArrayArbitrary`, lists of pairwise-distinct elementslike `arrayOf`, but element candidates colliding with another element are skipped`Gen::subset($values, $min, $max)``SubsetArbitrary`, 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::dictOf($key, $value, $min, $max)``DictionaryArbitrary`, 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)``RecordArbitrary`, fixed-shape map `['field' => $arb, ...]`each field via its arbitrary, key set fixed`Gen::elements($array)``OneOfArbitrary`, one value from an array (array form of `oneOf`)toward earlier-listed distinct values`Gen::enum(SomeEnum::class)``OneOfArbitrary` over the enum's casestoward earlier-declared cases (declare simpler cases first)`Gen::constant($value)``ConstantArbitrary`, always `$value`does not shrink`Gen::char()``StringArbitrary`, a single printable ASCII charactertoward `a``Gen::uuid()``UuidArbitrary`, RFC 4122 v4 UUID stringsdoes not shrink`Gen::datetime($min, $max)``DateTimeArbitrary`, 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)``FlatMappedArbitrary`, ordered pairs `[lo, hi]` with `lo  Gen::commands([], [
        Gen::map(Gen::intBetween(0, 99), static fn(int $v): Command => new Push($v)),
        Gen::constant(new Pop()),
    ])],
    parameterNames: ['sequence'],
    config: new PropertyConfig(runs: 200),
);

$result = (new PropertyRunner())->run($definition, new CallableTrialExecutor(
    static function (CommandSequence $sequence): void {
        StateMachine::check($sequence, static fn(): Stack => new Stack());
    },
));
```

### Exporting a counterexample

[](#exporting-a-counterexample)

`CounterExample` exposes `seed`, `runsBeforeFailure`, `originalArguments`, `shrunkArguments`, `shrinkSteps`, `shrinkTrials`, `skips` and the underlying `failure`; `toArray()`/`toJson()` return a normalized machine-readable form, and `toExamplesCode()` emits runnable PHP pinning the shrunk case as a permanent example.

`ValueRenderer::render($value)` produces the single-line human form used inside counterexample messages (strings quoted and escaped, arrays and objects summarised, recursion and depth bounded). Adapters reuse it so their verbose output reads exactly like the failure message.

### Debugging generators

[](#debugging-generators)

`Gen::sample($arb, $count, $seed)` eagerly generates values; `Gen::sampleShrinks($arb, $seed)` shows one value plus its first shrink candidates — the fastest way to check a custom arbitrary shrinks as intended.

Security
--------

[](#security)

The engine performs no I/O, SQL, shell, or network operations itself; the only filesystem access is the opt-in `FilesystemCorpus`, and only when you pass it to the runner. Random values come from PHP's MT19937 engine seeded by the reported seed — a PRNG, not a CSPRNG; never use generated values for cryptographic purposes, and treat seeds as reproducibility handles, not secrets.

When you do enable the corpus, remember that it persists failing inputs as plain JSON: see [Regression corpus](#regression-corpus) for what that means for generators that can produce sensitive-looking data.

Examples
--------

[](#examples)

See [examples/](examples/) for runnable scripts.

ScriptShowsNeeds server?`basic.php`a property that holds, one that is falsified, and tree-based shrinkingNo`generators.php``sample`, boundary bias, `uuid`, `datetime`, `dictOf`, `record`, `flatMap`No`standalone_runner.php`driving the engine directly: `PropertyDefinition`, `CallableTrialExecutor`, structured `PropertyResult`No`custom_listeners.php`a console reporter and a telemetry collector as pure `PropertyListener`sNoDevelopment
-----------

[](#development)

No PHP/Composer on the host. Run commands in Docker via the `composer:2` image:

```
docker run --rm -v "$PWD":/app -w /app composer:2 composer install
docker run --rm -v "$PWD":/app -w /app composer:2 composer build
docker run --rm -v "$PWD":/app -w /app composer:2 composer cs:fix
docker run --rm -v "$PWD":/app -w /app composer:2 composer test
docker run --rm -v "$PWD":/app -w /app composer:2 composer release-check
```

Or with Make:

```
make install
make build
make cs-fix
make test
make test-coverage
make mutation
make release-check
```

`make test-coverage` and `make mutation` bootstrap `pcov` inside the `composer:2` container because the base image has no coverage driver.

License
-------

[](#license)

[BSD-3-Clause](LICENSE.md)

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance100

Actively maintained with recent releases

Popularity14

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity40

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b0812d5572a7041dfe36e222d295b2e6dc55833a605350fcde58a51a5965ed30?d=identicon)[rasuvaeff](/maintainers/rasuvaeff)

---

Top Contributors

[![rasuvaeff](https://avatars.githubusercontent.com/u/1352718?v=4)](https://github.com/rasuvaeff "rasuvaeff (17 commits)")

---

Tags

framework-agnosticfuzzinghedgehogphpphp8property-based-testingproperty-testingquickcheckshrinkingtestingtestinggeneratorsQuickCheckproperty-testingfuzzingshrinking

###  Code Quality

Static AnalysisPsalm, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/rasuvaeff-property-testing-core/health.svg)

```
[![Health](https://phpackages.com/badges/rasuvaeff-property-testing-core/health.svg)](https://phpackages.com/packages/rasuvaeff-property-testing-core)
```

###  Alternatives

[phpunit/phpunit

The PHP Unit Testing framework.

20.0k971.0M164.8k](/packages/phpunit-phpunit)[phpunit/php-code-coverage

Library that provides collection, processing, and rendering functionality for PHP code coverage information.

8.9k951.4M1.7k](/packages/phpunit-php-code-coverage)[mockery/mockery

Mockery is a simple yet flexible PHP mock object framework

10.7k536.9M28.4k](/packages/mockery-mockery)[behat/behat

Scenario-oriented BDD framework for PHP

4.0k103.4M2.3k](/packages/behat-behat)[symfony/phpunit-bridge

Provides utilities for PHPUnit, especially user deprecation notices management

2.5k216.1M5.4k](/packages/symfony-phpunit-bridge)[brianium/paratest

Parallel testing for PHP

2.5k142.8M1.1k](/packages/brianium-paratest)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
