Skip to content
Pest Flow
Esc
↑↓navigate↵open⌘Jpreview
On this page

Core concepts

Learn how Pest Flow's feature and scenario DSL maps to Pest tests.

Pest Flow organizes ordinary Pest tests into a behaviour tree. It does not introduce another test runner or assertion language.

Feature       Pest describe group
└── Rule      nested Pest describe group
    └── Scenario  Pest test (it)
        ├── Given  setup step
        ├── When   action step
        └── Then   assertion step

Feature

A feature names a capability or domain area, such as account registration or contractor activation. feature() creates a Pest describe group. Use Pest hooks such as beforeEach() inside the feature callback to set up state shared by its rules and scenarios.

Features cannot be nested. A feature can contain rules; it cannot contain scenarios directly.

Use ->tags(...) on a feature declaration to label it. Its tags are inherited by descendant scenarios for Pest group filtering. See tags and filtering.

Rule

A rule states a business constraint within a feature. rule() creates a nested Pest describe group, so Pest’s hooks defined in the feature and rule scopes apply to their tests.

A rule must be declared inside a feature. Rules cannot be nested inside other rules.

Use ->tags(...) on a rule declaration to label it and make the tag available to its scenarios when filtering.

Scenario

A scenario is one concrete example of a rule. scenario() registers it as a normal Pest test, with the scenario name as the test description. A scenario inside a feature must be nested inside a rule. A standalone scenario at the top level is also allowed when a feature/rule hierarchy would add no value.

Use ->tags(...) after a scenario declaration to label that scenario directly.

Pest remains responsible for test discovery, execution, hooks, assertions, and reporting. A scenario can use any Pest expectation or PHP code supported by the test environment.

Given, When, Then

These helpers label the parts of a scenario:

  • Given establishes the starting state.
  • When performs the action under test.
  • Then checks the outcome, usually with Pest expectations.

When a scenario runs, Pest Flow first records its step declarations, then executes their closures in declaration order. All step closures share the same Pest test object, so values assigned through $this in one step are available in the next. If a step throws or an expectation fails, Pest Flow records the exception and rethrows it to Pest. Later step closures do not run, and their recorded nodes receive the Skipped status.

Keep scenario callbacks focused on declaring steps. Statements outside step closures run during the declaration pass, before any step closure executes.

The labels are conventions, not a validator: Pest Flow does not require exactly one step of each type or enforce a particular Given/When/Then order. It records the type, description, and source location of every declared step, including steps skipped after a failure.

Test hooks and context

Because features and rules map to nested Pest describe groups, Pest lifecycle hooks belong at the scope where their setup applies:

feature('Contractor activation', function (): void {
    beforeEach(function (): void {
        $this->contractor = ['compliant' => true, 'active' => false];
    });

    rule('Only compliant contractors may activate', function (): void {
        beforeEach(function (): void {
            $this->auditLog = [];
        });

        scenario('Activates a compliant contractor', function (): void {
            given('a compliant contractor', function (): void {
                $this->auditLog[] = 'loaded';
            });

            when('they activate', function (): void {
                $this->contractor['active'] = true;
            });

            then('activation is recorded', function (): void {
                expect($this->contractor['active'])->toBeTrue()
                    ->and($this->auditLog)->toBe(['loaded']);
            });
        });
    });
});

Use non-static closures for callbacks that need Pest’s $this context. Pest Flow calls each step closure with the active test object as its context.

Behaviour registry

FlowRegistry exposes the declared behaviour tree to PHP:

use Pest\Flow\FlowRegistry;

$features = FlowRegistry::features();
$rules = FlowRegistry::rules();
$scenarios = FlowRegistry::scenarios();
$steps = FlowRegistry::steps();

Features, rules, and scenarios are registered while Pest loads the test definitions. Step nodes are added when their scenario runs, before step closures execute. Scenario and step nodes expose execution status, duration, and any captured exception. The registry is process-local. Read the registry reference for timing, fields, and identifier rules.

Next steps

Was this page helpful?