State machine: концепции
Stateful / model-based тестирование
Некоторые баги проступают только в последовательности операций — счётчик, переполняющийся после N инкрементов, кэш, возвращающий устаревшие данные, стек, теряющий порядок. Model-based-тестирование генерирует случайные последовательности команд, прогоняет каждую против реальной системы, зеркалируя её в упрощённой модели, а при падении shrink'ает последовательность до кратчайшей, всё ещё ломающей поведение.
Реализуйте Command — четыре почти чистых ответственности плюс метку:
| Метод | Назначение |
|---|---|
preCondition(mixed $model): bool | Может ли эта команда выполниться в текущем состоянии модели? Гейтит генерацию и при replay — выполняется команда или пропускается. |
nextState(mixed $model): mixed | Ожидаемое следующее состояние модели (чисто; возвращает новую модель, ничего не мутирует). |
run(mixed $model, mixed $system): mixed | Выполнить против системы под тестом; вернуть наблюдаемый результат. |
postCondition(mixed $model, mixed $result): bool | Проверить результат против модели предсостояния. Вернуть false (или бросить), чтобы фальсифицировать. |
__toString(): string | Метка для трассировки контрпримера. |
Gen::commands($initialModel, $commandGenerators) строит валидные последовательности (каждый шаг добавляет команду с выполненным предусловием, затем двигает модель), а StateMachine::check() гоняет сгенерированную последовательность против свежей системы:
use Rasuvaeff\PropertyTesting\Gen;
use Rasuvaeff\PropertyTesting\Property;
use Rasuvaeff\PropertyTesting\StateMachine\CommandSequence;
use Rasuvaeff\PropertyTesting\StateMachine\StateMachine;
use Testo\Test;
#[Test]
final class StackModelTest
{
#[Property(runs: 200)]
public function stackBehavesLikeItsModel(CommandSequence $sequence): void
{
StateMachine::check($sequence, static fn(): Stack => new Stack());
}
/** @return array<string, \Rasuvaeff\PropertyTesting\ArbitraryInterface> */
public static function stackBehavesLikeItsModelGenerators(): array
{
return ['sequence' => Gen::commands([], [
Gen::map(Gen::intBetween(0, 99), static fn(int $v): Command => new Push($v)),
Gen::constant(new Pop()),
])];
}
}Shrinking удаляет целые блоки команд (вплоть до одной, чтобы падающий шаг в середине изолировать), а затем упрощает параметры каждой команды через её собственное дерево. Поскольку раннер перепроверяет каждое предусловие и пропускает любой шаг, инвалидированный удалённым/упрощённым шагом, каждая shrunk-последовательность остаётся корректной. Контрпример рендерится как читаемая трасса, а нарушенное постусловие бросает PostconditionViolation с указанием шага:
Property falsified after 7 successful run(s); seed=42
Original: sequence=[Push(3), Pop(), Push(5), Push(1), Pop(), Pop()]
Shrunk: sequence=[Push(0), Push(1), Pop()] (9 shrink step(s))
Failure: Postcondition failed at step 3 for command Pop(); sequence: [Push(0), Push(1), Pop()]Полный пример стека — см. examples/state_machine.php.