Skip to content

Migrating from property-testing 2.x ​

rasuvaeff/property-testing 2.x was a single package: a property-based testing engine welded to a Testo plugin. It is frozen at 2.8.1 and marked abandoned. The same code now ships as three packages — the engine plus one adapter per test framework — so a project pulls only the framework it actually uses.

The split was designed as a drop-in for the public API: no public fully qualified class name changed, no method convention changed, no environment variable changed, and the regression corpus on disk is read back byte-for-byte. For a Testo project the whole migration is two Composer commands and no PHP edits.

That guarantee covers what 2.x documented as public. Classes marked @internal in 2.x are the one exception — some moved, and code that reached for them has imports to update; see Custom harness for the exact mapping.

Pick your path ​

You were usingInstallPHP code changes
#[Property] under Testorasuvaeff/property-testing-testonone
The engine directly (custom harness, CLI script, CI guard)rasuvaeff/property-testing-corenone for public API; update imports of the @internal classes listed under Custom harness
Nothing yet, and you test with PHPUnitrasuvaeff/property-testing-phpunitnew integration, see PHPUnit

Testo ​

bash
composer remove --dev rasuvaeff/property-testing
composer require --dev "rasuvaeff/property-testing-testo:^1.0" -W

That is the whole migration. Every import, attribute, generator method and assertion stays as it is:

php
use Rasuvaeff\PropertyTesting\ArbitraryInterface;
use Rasuvaeff\PropertyTesting\Gen;
use Rasuvaeff\PropertyTesting\Property;
use Testo\Assert;

#[Property(runs: 300)]
public function roundTrip(string $value): void
{
    Assert::same(decode(encode($value)), $value);
}

/** @return array<string, ArbitraryInterface> */
public static function roundTripGenerators(): array
{
    return ['value' => Gen::string()];
}

Two things about that command are not optional:

  • composer remove must come first. The engine declares conflict: {"rasuvaeff/property-testing": "*"}, because both packages ship classes in the Rasuvaeff\PropertyTesting namespace. A mixed install is deliberately unsolvable rather than silently duplicated on the autoloader. Requiring the adapter while 2.x is still in composer.json fails with Your requirements could not be resolved.
  • -W (--with-all-dependencies). The Testo adapter requires testo/testo ^0.10.39. If your lock file pins an older testo/testo, Composer refuses the install without permission to raise it; -W lets it bump testo/testo and its satellites within their allowed ranges.

What is guaranteed to keep working ​

Class namesEvery public FQCN — Gen, ArbitraryInterface, Shrinkable, Assume, Classify, Property, CounterExample, the StateMachine namespace, and every public exception
Conventions<method>Generators() and <method>Examples(), resolved by reflection exactly as before
EnvironmentPROPERTY_RUNS, PROPERTY_SEED, PROPERTY_DB, PROPERTY_VERBOSE — same semantics, same precedence, same validation errors
MessagesThe counterexample message format is unchanged and pinned by golden tests
CorpusSame FORMAT_VERSION, same JSON, same file layout. A corpus written by 2.8 replays under the adapter — your CI regression corpora keep their value
SeedsSEQUENCE_EPOCH was not bumped: a given seed still produces the same inputs
CoveragePer-run Testo TestResult attributes, including codecov's CoverageResult, are still merged onto the aggregate result — property tests stay visible to per-test coverage and to Infection

CI ​

Nothing changes. The PROPERTY_DB regression-corpus cache recipe — restore before the test step, PROPERTY_DB on the step itself, save after it with if: ${{ !cancelled() }} — works identically, because the corpus format and the variable are the same. The full recipe is on Regression corpus.

Custom harness ​

If you were driving the engine yourself, install the engine alone:

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

No test framework comes with it. Build a PropertyDefinition, hand it to PropertyRunner through a TrialExecutor, and inspect the structured PropertyResult — see Examples and examples/standalone_runner.php.

Under 2.x this was only half-possible: the runner did not exist as a separate object until the split, so a custom harness had to reach into Rasuvaeff\PropertyTesting\Internal. Those classes moved, and the ones a harness actually needs became public in the move:

2.x (@internal)Core
Internal\CorpusStorageRunner\FilesystemCorpus (@api), constructed with the directory to write under; the runner itself never reads the environment
Internal\CorpusEntryRunner\CorpusEntry (@api)
Internal\Clock, Internal\MonotonicClockRunner\Clock, Runner\MonotonicClock (@api) — the clock is a constructor argument of PropertyRunner, so deadline and budget behaviour is testable deterministically
Internal\ValueRendererValueRenderer at the package root (@api) — adapters need it for verbose output and messages
Internal\PropertyInterceptor, Internal\TestoTrialExecutor, Internal\VerboseListenerNot in core. They are Testo-specific and live in rasuvaeff/property-testing-testo

Internal\Boundary, Internal\DrawContext, Internal\RegexCompiler and Internal\ValueCodec keep both their names and their @internal status.

Reading environment variables is the adapter's job, not the engine's: core never touches getenv(). If your harness wants the PROPERTY_* contract, read the variables yourself and pass the resulting PropertyConfig and Corpus in:

php
$corpus = CorpusFactory::fromDsn(
    EnvironmentOverrides::string(getenv('PROPERTY_DB')) ?? sys_get_temp_dir() . '/property-db',
);

CorpusFactory::fromDsn() is the one entry point for the PROPERTY_DB half: it returns a FilesystemCorpus for a directory path and a RedisCorpus for a redis:// DSN, and refuses any other scheme. There is deliberately no helper that reads the variable for you — one existed until 0.10 and built a directory named after whatever it was handed, so a Redis DSN quietly became a directory called redis:/host:port. The variables themselves are documented on Environment overrides.

PHPUnit ​

There was no PHPUnit integration in 2.x — installing it into a PHPUnit project dragged in testo/testo transitively, which is exactly the coupling the split removes.

bash
composer require --dev rasuvaeff/property-testing-phpunit

The API is a trait with a fluent chain rather than an attribute, because PHPUnit's public extension API observes test execution but offers no supported way to intercept and re-run a test method:

php
use PHPUnit\Framework\TestCase;
use Rasuvaeff\PropertyTesting\Gen;
use Rasuvaeff\PropertyTesting\PhpUnit\PropertyTesting;

final class SortPropertyTest extends TestCase
{
    use PropertyTesting;

    public function testSortIsIdempotent(): void
    {
        $this->forAll(['values' => Gen::arrayOf(Gen::int())])
            ->runs(300)
            ->check(static function (array $values): void {
                sort($values);
                $once = $values;
                sort($values);

                self::assertSame($once, $values);
            });
    }
}

Generators, shrinking, the corpus and the PROPERTY_* variables behave exactly as under Testo — the two adapters share this engine and the parity is pinned by tests. Failures arrive as a single AssertionFailedError carrying the engine's message, with the engine exception as previous. See PHPUnit adapter for the full fluent chain.

Both adapters in one project ​

Supported. They share this engine, declare no global registration, and keep their framework-specific classes in separate sub-namespaces (Rasuvaeff\PropertyTesting\Testo, Rasuvaeff\PropertyTesting\PhpUnit).

Versioning ​

Adapters pin the engine with a caret range on its current major, and the three version numbers are not kept in sync with each other. What each number promises — the @api surface, seed stability, the corpus format, message texts, events, constructors — is written out in Compatibility policy; read that before treating a minor upgrade as risky.

The engine, both adapters and -names are at 1.0: from here on, anything the policy lists as frozen changes only with a major.

Staying on 2.x ​

rasuvaeff/property-testing still installs and still works. It receives security fixes only: no new features, no 3.0. Composer will report it as abandoned and suggest rasuvaeff/property-testing-testo.