Skip to content

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() гоняет сгенерированную последовательность против свежей системы:

php
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.