定制 rasuvaeff/property-testing 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

rasuvaeff/property-testing

Composer 安装命令:

composer require --dev rasuvaeff/property-testing

包简介

Property-based testing plugin for Testo

README 文档

README

Latest Stable Version Total Downloads Build Static analysis Psalm level PHP License

Property-based testing for PHP 8.3+, built as a plugin for the Testo testing framework. Generate hundreds of random inputs per test, find the failing one, and shrink it to a minimal counterexample you can actually read.

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

Requirements

  • PHP 8.3+
  • ext-mbstring
  • ext-random
  • testo/testo ^0.10.25 || ^1.0

Installation

composer require --dev rasuvaeff/property-testing

No plugin registration is needed: the #[Property] attribute self-registers with Testo through the framework's interceptor discovery.

Usage

Mark a test method with #[Property] and point it at a generators method that maps each parameter name to a Gen factory. The runner generates random arguments, runs the property runs times, and on the first failure shrinks the counterexample to a minimal one.

use Rasuvaeff\PropertyTesting\Assume;
use Rasuvaeff\PropertyTesting\Gen;
use Rasuvaeff\PropertyTesting\Property;
use Testo\Assert;
use Testo\Test;

#[Test]
final class RetryPolicyPropertyTest
{
    #[Property(runs: 500, generators: 'delayGenerators')]
    public function delayNeverExceedsCap(int $maxAttempts, int $baseSeconds, int $cap, int $attempts): void
    {
        Assume::that($cap >= $baseSeconds);

        $policy = WebhookRetryPolicy::exponential($maxAttempts, $baseSeconds, $cap);

        Assert::true($policy->nextDelaySeconds($attempts) <= $cap);
    }

    /** @return array<string, \Rasuvaeff\PropertyTesting\ArbitraryInterface> */
    private function delayGenerators(): array
    {
        return [
            'maxAttempts' => Gen::intBetween(1, 50),
            'baseSeconds' => Gen::intBetween(1, 300),
            'cap' => Gen::intBetween(1, 86400),
            'attempts' => Gen::intBetween(1, 100),
        ];
    }
}

On failure, the counterexample is rendered into the test output:

Property falsified after 246 successful run(s); seed=7382910
  Original: maxAttempts=17, baseSeconds=91, cap=847, attempts=23
  Shrunk:   maxAttempts=1, baseSeconds=848, cap=847, attempts=1 (12 shrink step(s))

Reproduce the exact run by passing the reported seed back to the attribute:

#[Property(runs: 500, seed: 7382910, generators: 'delayGenerators')]

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.

Generators

Factory Produces Shrinks
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 -> false
Gen::string() StringArbitrary, Unicode, length 0..100 toward '', then by length, then each character toward a
Gen::stringAscii() StringArbitrary, printable ASCII, length 0..100 toward '', then by length, then each character toward a
Gen::stringOf($min, $max) StringArbitrary, Unicode, bounded length toward '', then by length, then each character toward a
Gen::arrayOf($element) ArrayArbitrary, lists of $element, size 0..100 toward [], then by length, then each element
Gen::nonEmptyArrayOf($element) ArrayArbitrary, non-empty lists by length (never below 1), then each element
Gen::oneOf(...$values) OneOfArbitrary, one of the given values each distinct other value
Gen::nullable($inner) NullableArbitrary, null or an $inner value prefers null
Gen::map($inner, $fn) MappedArbitrary, $inner transformed by $fn no shrinking (mapping may not be invertible)
Gen::filter($inner, $predicate) FilteredArbitrary, $inner values satisfying $predicate delegates, keeping predicate-satisfying candidates
Gen::tuple(...$elements) TupleArbitrary, fixed-arity tuple, one value per element each position via its element, arity fixed
Gen::frequency($pairs) FrequencyArbitrary, weighted choice over [weight, arbitrary] pairs delegates to the inner arbitraries

Assume::that()

Discards the current run when a precondition does not hold. Discarded runs are neither failures nor successful checks. Prefer it over Gen::filter() when the rejection rate is low; when more than 90% of runs are discarded the runner warns that the generators are likely misconfigured.

Assume::that($cap >= $baseSeconds);

Security

This package executes test methods via reflection (to read the #[Property] attribute and invoke the generators method) and through Testo's pipeline. The fallback Testo interceptor is PropertyInterceptor. It performs no I/O, SQL, shell, or network operations itself. Random values are generated with PHP's MT19937 engine seeded by the reported seed; do not rely on them for cryptographic purposes.

Examples

See examples/ for runnable scripts.

Script Shows Needs server?
basic.php a property that holds, one that is falsified, and shrinking No

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

BSD-3-Clause

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: BSD-3-Clause
  • 更新时间: 2026-06-29