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:
// Step text from Gherkin
$text = 'a user "John" exists';
// Find matching definition
$match = $registry->find($text, StepType::Given);
// Returns: MatchResult with definition + extracted valuesMatch Priority
When multiple patterns could match:
- Exact match - Pattern matches completely
- Most specific - Fewer wildcards preferred
- First registered - Earlier definitions win
// 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 patternParameter Extraction
After matching, values are extracted based on type hints:
#[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:
| Type | Raw Value | Cast 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 errorAlias Injection
Given a user "John" exists as {buyer}#[When('buyer makes purchase')]
public function purchase(User $buyer): void
{
// $buyer matched by parameter name "buyer"
}Type Injection
#[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:
#[When('order {id} is processed')]
public function process(Order $order): void
{
// If Order not in context, queries: Order::find($id)
}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:
- Parameter type extends
Illuminate\Database\Eloquent\Model - No instance of that type exists in context
- The extracted value is numeric
After querying, the model is stored in context for future steps.
Mixed Resolution
Parameters can come from multiple sources:
#[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
// 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
// Call directly on class
$result = User::createWithName($name);No instance is created for static methods.
Functions
// 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 ResultResult Storage
Method return values are stored in ScenarioContext:
#[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 $userType Hierarchy Storage
Objects are stored by their full class hierarchy:
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 $adminAlias Storage
When using the as {alias} syntax:
Given a user "John" exists as {buyer}$context->storeWithAlias($user, 'buyer');
// Retrievable by alias
$context->getByAlias('buyer'); // Returns $userLast Result
The most recent return value is always available:
$context->getLastResult(); // Latest return valueError 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 existsExecution Error
When the step method throws:
StepExecutionException: Step failed: When payment is processed
Caused by: PaymentException: Card declined
In: PaymentSteps::processPayment at line 45Scenario 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:
// 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:
// 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:
// First check queries app() function
$available = $resolver->isLaravelAvailable();
// Subsequent calls use cached result
$cached = $resolver->isLaravelAvailable();