Skip to content

Auto-Injection

Auto-injection is one of Pest BDD's most powerful features. It automatically provides objects created in earlier steps to later steps, eliminating manual state management.

How It Works

When a step method returns an object, it's stored in the scenario context. Later steps can receive that object by declaring a parameter of the matching type.

php
// Step 1: Create and return a User
#[Given('a user {name} exists')]
public function userExists(string $name): User
{
    return User::factory()->create(['name' => $name]);
}

// Step 2: User is automatically injected
#[When('the user logs in')]
public function login(User $user): void
{
    // $user is the same instance from Step 1
    $this->actingAs($user);
}

// Step 3: User is still available
#[Then('the user should see the dashboard')]
public function seeDashboard(User $user): void
{
    // Same $user instance
    expect($user->isLoggedIn())->toBeTrue();
}

ScenarioContext Storage

The ScenarioContext class manages three types of storage:

1. Type Storage

Objects stored by their class name (and parent classes/interfaces):

php
// When you return a User
return User::factory()->create();

// It's stored as:
$context->typeStorage['App\Models\User'] = $user;
$context->typeStorage['Illuminate\Database\Eloquent\Model'] = $user;
$context->typeStorage['Illuminate\Contracts\Auth\Authenticatable'] = $user;

This means you can request it by any of these types:

php
public function step(User $user): void { }           // Works
public function step(Model $model): void { }         // Works (gets User)
public function step(Authenticatable $auth): void { } // Works (gets User)

2. Alias Storage

Objects stored by a custom name:

gherkin
Given a user "John" exists as {admin}
php
$context->aliasStorage['admin'] = $user;

Access by parameter name:

php
public function step(User $admin): void
{
    // $admin injected because parameter name matches alias
}

3. Instance Storage

Reusable step class instances:

php
$context->instances['CalculatorSteps'] = $stepInstance;

This allows state to persist within step classes during a scenario.

Resolution Order

When resolving a parameter, Pest BDD tries these sources in order:

1. Alias Injection (Highest Priority)

If parameter name matches an alias:

php
// Given a user exists as {buyer}
public function step(User $buyer): void
{
    // $buyer found by alias "buyer"
}

2. Type Injection

If parameter type is a class and exists in context:

php
// Previous step returned a User
public function step(User $user): void
{
    // $user found by type User
}

Also checks parent classes and interfaces:

php
public function step(Model $model): void
{
    // Gets the User if no direct Model match
}

3. Extracted Values

If parameter name matches a pattern placeholder:

php
#[Given('there are {count} items')]
public function step(int $count): void
{
    // $count extracted from step text
}

4. Model Query Fallback

If parameter type is an Eloquent Model and extracted value is numeric:

php
#[When('order {id} is processed')]
public function process(Order $order): void
{
    // If no Order in context, queries Order::find($id)
}

This fallback enables referencing models by ID:

gherkin
Given order 42 exists in the database
When order 42 is processed
# $order = Order::find(42)

The query only happens when:

  • Parameter type extends Illuminate\Database\Eloquent\Model
  • No instance exists in context
  • Extracted value is numeric

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

5. Default Values

If parameter has a default:

php
public function step(int $count = 10): void
{
    // Uses 10 if count not extracted or injected
}

6. Nullable Types

If parameter is nullable:

php
public function step(?User $user): void
{
    // $user is null if no User in context
}

Practical Examples

Basic Injection

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

#[When('I add the product to cart')]
public function addToCart(Product $product, Cart $cart): void
{
    // $product injected from Given step
    // $cart injected if exists, or use default
    $cart->add($product);
}

#[Then('the cart should contain the product')]
public function cartContains(Product $product, Cart $cart): void
{
    expect($cart->items)->toContain($product);
}

Multiple Objects

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

#[Given('a post {title} exists')]
public function postExists(string $title): Post
{
    return Post::factory()->create(['title' => $title]);
}

#[When('the user comments on the post')]
public function userComments(User $user, Post $post): Comment
{
    // Both injected from previous steps
    return Comment::create([
        'user_id' => $user->id,
        'post_id' => $post->id,
        'body' => 'Great post!',
    ]);
}

#[Then('the post should have a comment')]
public function postHasComment(Post $post, Comment $comment): void
{
    expect($post->comments)->toContain($comment);
}

Alias-Based Injection

gherkin
Scenario: Transfer between accounts
  Given a user "Alice" exists as {sender}
  And a user "Bob" exists as {receiver}
  When sender transfers to receiver
  Then sender balance should decrease
  And receiver balance should increase
php
#[When('sender transfers to receiver')]
public function transfer(User $sender, User $receiver): void
{
    // $sender matched by alias "sender"
    // $receiver matched by alias "receiver"
    $sender->transferTo($receiver, 100);
}

#[Then('sender balance should decrease')]
public function senderBalanceDecreased(User $sender): void
{
    expect($sender->fresh()->balance)->toBeLessThan(1000);
}

#[Then('receiver balance should increase')]
public function receiverBalanceIncreased(User $receiver): void
{
    expect($receiver->fresh()->balance)->toBeGreaterThan(0);
}

Mixed Injection and Extraction

php
#[When('the user purchases {quantity} items')]
public function purchase(User $user, int $quantity): Order
{
    // $user - injected from context
    // $quantity - extracted from step text
    return Order::create([
        'user_id' => $user->id,
        'quantity' => $quantity,
    ]);
}

Origin Tracking

ScenarioContext tracks where each object came from, useful for debugging:

php
// When storing
$context->store($user, 'admin');
$context->typeOrigins[User::class] = 'Given a user "John" exists';
$context->aliasOrigins['admin'] = 'Given a user "John" exists as {admin}';

When injection fails, error messages include origin information:

Failed to resolve parameter $user in step "Then the user should be active"

Available in context:
  - User (from "Given a user exists")
  - Order (from "When the user places an order")

Did you mean to use one of these?

Inheritance Support

Objects are stored by their full class hierarchy:

php
class AdminUser extends User implements AdminInterface { }

#[Given('an admin exists')]
public function adminExists(): AdminUser
{
    return AdminUser::factory()->create();
}

// All of these work:
public function step(AdminUser $admin): void { }
public function step(User $user): void { }
public function step(Model $model): void { }
public function step(AdminInterface $admin): void { }

Interface-Based Injection

Request by interface for flexibility:

php
interface PaymentGateway { }
class StripeGateway implements PaymentGateway { }

#[Given('Stripe is configured')]
public function stripeConfigured(): StripeGateway
{
    return new StripeGateway(config('stripe'));
}

#[When('I process payment')]
public function processPayment(PaymentGateway $gateway): void
{
    // Gets StripeGateway via interface
    $gateway->charge(100);
}

Best Practices

Return Values for Injection

Always return objects you want available later:

php
// Good: Returns for injection
#[Given('a user exists')]
public function userExists(): User
{
    return User::factory()->create();
}

// Less useful: No return
#[Given('a user exists')]
public function userExists(): void
{
    User::factory()->create();  // Lost!
}

Use Specific Types

Be specific about what you need:

php
// Good: Specific type
public function step(User $user): void { }

// Avoid: Too generic
public function step(Model $model): void { }

Alias for Multiple Same-Type Objects

When you need multiple instances of the same type:

gherkin
Given a user exists as {buyer}
Given a user exists as {seller}
php
public function step(User $buyer, User $seller): void
{
    // Clear which is which
}

Document Injection Dependencies

php
/**
 * Processes payment for the current order.
 *
 * @requires User from "Given a user exists"
 * @requires Order from "When the user creates an order"
 */
#[When('the user pays')]
public function userPays(User $user, Order $order): Payment
{
    return Payment::process($user, $order);
}

Released under the MIT License.