Skip to content

Step Definitions

Step definitions bind your Gherkin steps to PHP code. In Pest BDD, you use PHP 8 attributes to mark methods as step handlers.

Basic Syntax

Use #[Given], #[When], or #[Then] attributes on methods:

php
use TestFlowLabs\PestTestAttributes\Given;
use TestFlowLabs\PestTestAttributes\Then;
use TestFlowLabs\PestTestAttributes\When;

class UserSteps
{
    #[Given('a user exists')]
    public function userExists(): void
    {
        // Setup code
    }

    #[When('I login')]
    public function login(): void
    {
        // Action code
    }

    #[Then('I should be logged in')]
    public function shouldBeLoggedIn(): void
    {
        // Assertion code
    }
}

Pattern Placeholders

Use {name} syntax to extract values from step text:

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

#[Given('there are {count} products')]
public function productsExist(int $count): void
{
    Product::factory()->count($count)->create();
}

The placeholder name should match the method parameter name.

Type Inference

Pest BDD automatically generates regex patterns based on PHP type hints:

Type HintGenerated RegexMatches
string"[^"]*""quoted text"
int-?\d+42, -5
float-?\d*\.?\d+3.14, .5, -2.5
booltrue|false|yes|no|1|0true, yes, 0

String Parameters

Strings must be quoted in the Gherkin:

gherkin
Given a user "John Doe" exists
php
#[Given('a user {name} exists')]
public function userExists(string $name): void
{
    // $name = "John Doe"
}

Integer Parameters

Integers are bare numbers:

gherkin
When I add 5 items to the cart
php
#[When('I add {count} items to the cart')]
public function addItems(int $count): void
{
    // $count = 5
}

Float Parameters

Floats support decimals:

gherkin
Then the price should be 99.99
php
#[Then('the price should be {price}')]
public function priceShouldBe(float $price): void
{
    // $price = 99.99
}

Boolean Parameters

Booleans accept multiple formats:

gherkin
Given notifications are true
Given notifications are yes
Given notifications are 1
php
#[Given('notifications are {enabled}')]
public function setNotifications(bool $enabled): void
{
    // $enabled = true for all above examples
}

Class Organization

Domain-Based Separation

Organize steps by domain, separating Given/When/Then by their natural locations:

php
// database/factories/UserFactory.php - Given steps
class UserFactory extends Factory
{
    #[Given('I am logged in')]
    public function definition(): array { /* ... */ }

    #[Given('an admin user exists')]
    public function admin(): static { /* ... */ }
}

// app/Actions/Auth/LoginUser.php - When steps (production code)
class LoginUser
{
    #[When('I login with {email}')]
    public function execute(string $email): User { /* ... */ }
}

// tests/Assertions/User/AuthAssertions.php - Then steps
class AuthAssertions
{
    #[Then('I should be logged in')]
    public function shouldBeLoggedIn(User $user): void
    {
        expect(auth()->check())->toBeTrue();
    }
}

State Management

Use instance properties to maintain state within a scenario:

php
class CalculatorSteps
{
    private int $result = 0;

    #[Given('I start with {n}')]
    public function startWith(int $n): void
    {
        $this->result = $n;
    }

    #[When('I add {n}')]
    public function add(int $n): void
    {
        $this->result += $n;
    }

    #[Then('the result is {expected}')]
    public function resultIs(int $expected): void
    {
        expect($this->result)->toBe($expected);
    }
}

Each scenario gets a fresh instance—state doesn't leak between scenarios.

Return Values and Context

Return values from steps are stored in the scenario context and can be auto-injected into later steps:

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

#[When('the user updates their email to {email}')]
public function updateEmail(User $user, string $email): void
{
    // $user is automatically injected from the previous step's return value
    $user->update(['email' => $email]);
}

#[Then('the user email should be {email}')]
public function emailShouldBe(User $user, string $email): void
{
    expect($user->fresh()->email)->toBe($email);
}

Learn more about this in Auto-Injection.

Multi-Language Support

Use repeatable attributes for the same step in multiple languages:

php
#[Given('a user exists')]
#[Given('bir kullanıcı mevcut')]        // Turkish
#[Given('ein Benutzer existiert')]      // German
public function userExists(): User
{
    return User::factory()->create();
}

See Multi-Language Support for details.

And/But Steps

And and But steps use the type of the previous step:

gherkin
Given a user exists
And the user is an admin      # This is treated as a Given
When I login
And I click dashboard         # This is treated as a When
Then I should see settings
But I should not see billing  # This is treated as a Then

You only need to define the step once with its base type:

php
#[Given('the user is an admin')]
public function userIsAdmin(): void { }

#[When('I click dashboard')]
public function clickDashboard(): void { }

#[Then('I should not see billing')]
public function shouldNotSeeBilling(): void { }

Assertions

Use Pest's expect() for fluent assertions:

php
#[Then('the user should be active')]
public function userShouldBeActive(User $user): void
{
    expect($user->is_active)->toBeTrue();
}

#[Then('there should be {count} products')]
public function shouldHaveProducts(int $count): void
{
    expect(Product::count())->toBe($count);
}

#[Then('the cart should contain {product}')]
public function cartShouldContain(string $product): void
{
    expect($this->cart->items)
        ->toHaveCount(1)
        ->sequence(
            fn ($item) => $item->name->toBe($product)
        );
}

Step Definition Patterns

Factory Pattern (Given)

Given steps often create entities:

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

#[Given('a product {name} with price {price}')]
public function productWithPrice(string $name, float $price): Product
{
    return Product::factory()->create([
        'name' => $name,
        'price' => $price,
    ]);
}

Action Pattern (When)

When steps perform actions:

php
#[When('I add {product} to the cart')]
public function addToCart(string $product): void
{
    $this->cart->add(
        Product::where('name', $product)->first()
    );
}

#[When('I checkout')]
public function checkout(): Order
{
    return $this->cart->checkout();
}

Assertion Pattern (Then)

Then steps verify outcomes:

php
#[Then('I should see {message}')]
public function shouldSeeMessage(string $message): void
{
    expect($this->response->content())->toContain($message);
}

#[Then('the order status should be {status}')]
public function orderStatusShouldBe(Order $order, string $status): void
{
    expect($order->fresh()->status)->toBe($status);
}

File Location

Step definitions follow the Given/When/Then pattern with clear locations:

database/
└── factories/                # Given steps - Factory'lerin kendisi
    ├── UserFactory.php       # #[Given('a user exists')]
    └── OrderFactory.php      # #[Given('an order exists')]

app/
└── Actions/                  # When steps - Production Actions
    ├── User/
    │   └── CreateUser.php    # #[When('I create a user {name}')]
    └── Order/
        └── PlaceOrder.php    # #[When('I place an order')]

tests/
├── Behaviors/                # Feature files
│   └── user-registration.feature
└── Assertions/               # Then steps - Test assertions only
    ├── User/
    │   └── UserAssertions.php
    ├── Order/
    │   └── OrderAssertions.php
    └── Shared/
        └── CommonAssertions.php

Organization Summary

Step TypeTDD EquivalentLocationDescription
GivenArrangedatabase/factories/Laravel Factory class'ları
WhenActapp/Actions/Production Action class'ları
ThenAsserttests/Assertions/Test assertion class'ları

Common Patterns

Reusable Steps

Create generic steps that work across scenarios:

php
#[Given('I am authenticated')]
public function authenticated(): User
{
    return $this->user = User::factory()->create();
}

#[Given('I am authenticated as {role}')]
public function authenticatedAs(string $role): User
{
    return $this->user = User::factory()->create(['role' => $role]);
}

Parameterized Assertions

php
#[Then('I should see {count} {entity}')]
public function shouldSeeCount(int $count, string $entity): void
{
    $model = match($entity) {
        'users' => User::class,
        'products' => Product::class,
        'orders' => Order::class,
    };

    expect($model::count())->toBe($count);
}

Negative Assertions

php
#[Then('I should not see {text}')]
public function shouldNotSee(string $text): void
{
    expect($this->response->content())->not->toContain($text);
}

#[Then('{product} should not be in the cart')]
public function productNotInCart(string $product): void
{
    expect($this->cart->contains($product))->toBeFalse();
}

Released under the MIT License.