Skip to content

Step Execution

This document explains how Pest BDD executes step definitions, from pattern matching to method invocation and result storage.

Execution Flow Overview

When a Gherkin step is executed, Pest BDD follows this sequence:

Step Text: "Given a user John exists"


┌─────────────────────────────────────┐
│ 1. Pattern Matching                 │
│    Find matching step definition    │
└──────────────────┬──────────────────┘


┌─────────────────────────────────────┐
│ 2. Parameter Extraction             │
│    Extract values from step text    │
└──────────────────┬──────────────────┘


┌─────────────────────────────────────┐
│ 3. Parameter Resolution             │
│    Combine extracted + injected     │
└──────────────────┬──────────────────┘


┌─────────────────────────────────────┐
│ 4. Method Invocation                │
│    Call the step method             │
└──────────────────┬──────────────────┘


┌─────────────────────────────────────┐
│ 5. Result Storage                   │
│    Store return value in context    │
└─────────────────────────────────────┘

Pattern Matching

The StepRegistry finds matching step definitions:

php
// Step text from Gherkin
$text = 'a user "John" exists';

// Find matching definition
$match = $registry->find($text, StepType::Given);
// Returns: MatchResult with definition + extracted values

Match Priority

When multiple patterns could match:

  1. Exact match - Pattern matches completely
  2. Most specific - Fewer wildcards preferred
  3. First registered - Earlier definitions win
php
// These might both match "a user John exists"
#[Given('a user {name} exists')]     // More specific
#[Given('{something} exists')]        // Less specific

// "a user John exists" matches first pattern

Parameter Extraction

After matching, values are extracted based on type hints:

php
#[Given('I have {count} items priced at {price}')]
public function haveItems(int $count, float $price): void

// Step: "I have 5 items priced at 29.99"
// Extracted: ['count' => '5', 'price' => '29.99']
// After casting: [5, 29.99]

Type casting rules:

TypeRaw ValueCast Result
string"John"John (quotes stripped)
int"42"42
float"3.14"3.14
bool"true"true

Parameter Resolution

The ParameterResolver determines each parameter's source:

Resolution Priority

For each method parameter, check in order:

1. Alias injection     → Parameter name matches stored alias
2. Type injection      → Parameter type exists in context
3. Model query         → Type is Model, value is numeric ID
4. Extracted value     → Value from pattern match
5. Default value       → Parameter has default
6. Nullable            → Type allows null
7. Unresolvable        → Throw error

Alias Injection

gherkin
Given a user "John" exists as {buyer}
php
#[When('buyer makes purchase')]
public function purchase(User $buyer): void
{
    // $buyer matched by parameter name "buyer"
}

Type Injection

php
#[Given('a user exists')]
public function createUser(): User
{
    return User::factory()->create();
}

#[When('user logs in')]
public function login(User $user): void
{
    // $user matched by type User
}

Model Query Fallback

When a parameter type is an Eloquent Model and the extracted value is numeric:

php
#[When('order {id} is processed')]
public function process(Order $order): void
{
    // If Order not in context, queries: Order::find($id)
}
gherkin
Given an order exists with id 123
When order 123 is processed
# $order = Order::find(123)

This fallback is useful when:

  • The model was created in a previous test or seeder
  • You're referencing existing database records
  • The ID comes from an external source

The query only happens if:

  1. Parameter type extends Illuminate\Database\Eloquent\Model
  2. No instance of that type exists in context
  3. The extracted value is numeric

After querying, the model is stored in context for future steps.

Mixed Resolution

Parameters can come from multiple sources:

php
#[When('user buys {quantity} of {product}')]
public function buy(User $user, int $quantity, string $product): void
{
    // $user     - Type injection (from context)
    // $quantity - Extracted value (from pattern)
    // $product  - Extracted value (from pattern)
}

Method Invocation

After parameters are resolved, the method is called:

Instance Methods

php
// 1. Resolve instance via container
$instance = app(OrderSteps::class);

// 2. Call method
$result = $instance->createOrder($user, $quantity);

Instance caching ensures the same step class instance is used throughout a scenario.

Static Methods

php
// Call directly on class
$result = User::createWithName($name);

No instance is created for static methods.

Functions

php
// Call the function directly
$result = create_test_user($name);

Invocation Diagram

StepDefinition

     ├─ isStaticMethod()? ─────────────────┐
     │        No                           │ Yes
     ├─ isFunction()? ────────┐            │
     │        No              │ Yes        │
     ▼                        ▼            ▼
getInstance()           call_user_func   ClassName::
     │                       │            method()
     ▼                       │                │
ContainerResolver            │                │
     │                       │                │
     ▼                       ▼                ▼
$instance->method()    function()         static call
     │                       │                │
     └───────────────────────┴────────────────┘


                      Store Result

Result Storage

Method return values are stored in ScenarioContext:

php
#[Given('a user {name} exists')]
public function createUser(string $name): User
{
    return User::factory()->create(['name' => $name]);
}

// After execution:
$context->store($user);  // Stored by type
$context->getByType(User::class);  // Returns $user

Type Hierarchy Storage

Objects are stored by their full class hierarchy:

php
class AdminUser extends User implements AdminInterface {}

$admin = new AdminUser();
$context->store($admin);

// All of these work:
$context->getByType(AdminUser::class);      // Returns $admin
$context->getByType(User::class);           // Returns $admin
$context->getByType(Model::class);          // Returns $admin
$context->getByType(AdminInterface::class); // Returns $admin

Alias Storage

When using the as {alias} syntax:

gherkin
Given a user "John" exists as {buyer}
php
$context->storeWithAlias($user, 'buyer');

// Retrievable by alias
$context->getByAlias('buyer');  // Returns $user

Last Result

The most recent return value is always available:

php
$context->getLastResult();  // Latest return value

Error Handling

Step Not Found

When no pattern matches:

StepNotFoundException: No step definition found for:
  Given user exists

Did you mean one of these?
  - "Given a user exists" (CalculatorSteps::createUser)
  - "Given a user {name} exists" (UserSteps::createNamed)

Parameter Resolution Failed

When a required parameter cannot be resolved:

ParameterResolutionException: Cannot resolve parameter $user for:
  When the user logs in

The parameter type 'User' was not found in context.

Did you forget a Given step? Add one like:
  Given a user exists

Execution Error

When the step method throws:

StepExecutionException: Step failed: When payment is processed

Caused by: PaymentException: Card declined

In: PaymentSteps::processPayment at line 45

Scenario Lifecycle

Understanding the full scenario execution:

┌────────────────────────────────────────┐
│           Scenario Start               │
│   - Create fresh ScenarioContext       │
│   - Initialize factory chain manager   │
└────────────────────┬───────────────────┘


┌────────────────────────────────────────┐
│         Background Steps               │
│   - Execute each Given step            │
│   - Store results in context           │
└────────────────────┬───────────────────┘


┌────────────────────────────────────────┐
│          Given Steps                   │
│   - Execute each Given step            │
│   - Factory chains accumulate          │
│   - Store results in context           │
└────────────────────┬───────────────────┘


┌────────────────────────────────────────┐
│    Given → When Transition             │
│   - Finalize all pending factories     │
│   - Create models from chains          │
└────────────────────┬───────────────────┘


┌────────────────────────────────────────┐
│           When Steps                   │
│   - Execute each When step             │
│   - Actions modify state               │
└────────────────────┬───────────────────┘


┌────────────────────────────────────────┐
│           Then Steps                   │
│   - Execute each Then step             │
│   - Assertions verify state            │
└────────────────────┬───────────────────┘


┌────────────────────────────────────────┐
│           Scenario End                 │
│   - Context is cleared                 │
│   - Ready for next scenario            │
└────────────────────────────────────────┘

Performance Optimization

Compiled Patterns

Step patterns are compiled once:

php
// First access compiles the regex
$compiled = CompiledStepDefinition::compile($definition);

// Subsequent matches use compiled version
$compiled->match($stepText);

Instance Caching

Step class instances are reused within a scenario:

php
// First access creates instance
$instance = $context->getInstance(OrderSteps::class);

// Subsequent calls return same instance
$same = $context->getInstance(OrderSteps::class);
assert($instance === $same);

Container Resolution Caching

Laravel availability is cached:

php
// First check queries app() function
$available = $resolver->isLaravelAvailable();

// Subsequent calls use cached result
$cached = $resolver->isLaravelAvailable();

Released under the MIT License.