---
title: Core concepts
description: 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.

~~~text
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](/pest-flow/tags).

## 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:

~~~php
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:

~~~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](/pest-flow/registry) for timing, fields, and identifier rules.

## Next steps

**[Complete walkthrough](/pest-flow/walkthroughs/first-scenario)**

Build a feature test one layer at a time.

**[DSL reference](/pest-flow/api)**

See the placement rules and behavior of each function.
