Рецепт: первое property
Проходим путь от пустого тест-класса до падающего property и обратно к зелёному на примере property_test.php.
1. Установка и Testo-тест
composer require --dev rasuvaeff/property-testingProperty — это обычный метод #[Test]-класса, помеченный #[Property] вместо утверждений на захардкоженных значениях:
use Rasuvaeff\PropertyTesting\Gen;
use Rasuvaeff\PropertyTesting\Property;
use Testo\Assert;
use Testo\Test;
#[Test]
final class ListReversalProperties
{
#[Property(runs: 200)]
public function reversingTwiceRestoresTheList(array $xs): void
{
Assert::same(array_reverse(array_reverse($xs)), $xs);
}
/** @return array<string, \Rasuvaeff\PropertyTesting\ArbitraryInterface> */
public static function reversingTwiceRestoresTheListGenerators(): array
{
return ['xs' => Gen::arrayOf(Gen::intBetween(-100, 100))];
}
}Две вещи делают это property, а не example-based тестом:
- Метод принимает параметр (
array $xs) вместо захардкоженного входа. - Соседний метод с именем
<methodName>Generators(или явно указанный через#[Property(generators: '...')]) возвращает по одномуArbitraryInterfaceна параметр, ключ — имя параметра. См. Генераторы — аргументы атрибутов должны быть constant expressions, поэтому генератор не может жить прямо внутри#[Property(...)].
Запуск:
docker run --rm -v "$PWD":/app -w /app composer:2 vendor/bin/testoruns: 200 — это 200 успешных проверок; отброшенные попытки (см. Assume vs filter) в этот счёт не идут.
2. Написать падающее — намеренно
Замените утверждение на неверный инвариант, чтобы увидеть фальсификацию property целиком:
#[Property(runs: 200)]
public function everyIntIsEven(int $n): void
{
Assert::true($n % 2 === 0);
}
/** @return array<string, \Rasuvaeff\PropertyTesting\ArbitraryInterface> */
public static function everyIntIsEvenGenerators(): array
{
return ['n' => Gen::intBetween(0, 1000)];
}Вывод выглядит так:
Property falsified after 3 successful run(s); seed=918273645
Original: n=847
Shrunk: n=1 (4 shrink step(s), 9 trial(s))Читайте справа налево:
seed— верните его через#[Property(seed: 918273645)], чтобы реплеить ровно этот прогон; см. Воспроизведение по seed.Original— первый упавший вход, без изменений.Shrunk— наименьший вход, который шринкер смог найти всё ещё падающим, достигнутый жадным спуском по shrink-дереву значения.1— действительно минимальное нечётное int, до которого шринкер может дойти, шринкая к0. См. Shrinking, как работает спуск и почему он завершается.shrink step(s)/trial(s)— принятые спуски против всех опробованных кандидатов (принятых + отклонённых).
3. Снова зелёный
Исправьте утверждение (или код под тестом) и перезапустите. Проходящее property ничего дополнительно не печатает — Testo сообщает о нём как о любом другом прошедшем тесте. Отдельного «режима property» для проверки нет; вывод выше — единственный дополнительный сигнал, который этот пакет добавляет поверх обычного падения assertion.
Куда дальше
- Концепции — словарь (arbitrary, shrinking, counterexample), которым эта страница пользовалась без определения.
- Обзор генераторов — полный каталог
Gen::*. - Воспроизведение по seed — когда встретите первое реальное падение и понадобится отладить его, а не просто прочитать однострочную сводку.