Skip to content

Step Definitions

Well-designed step definitions are reusable, maintainable, and clearly express intent.

Arrange-Act-Assert

Structure by Purpose

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

// When = Act (action)
#[When('the user places an order')]
public function placeOrder(User $user): Order
{
    return OrderService::create($user);
}

// Then = Assert (verify)
#[Then('the order should be confirmed')]
public function orderConfirmed(Order $order): void
{
    expect($order->status)->toBe('confirmed');
}

Return Objects

Enable Auto-Injection

Always return objects that later steps might need:

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

// Avoid: No return
#[Given('a product exists')]
public function productExists(): void
{
    Product::factory()->create(); // Can't be injected!
}

Chain Returns Through Steps

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

#[When('the user creates an order')]
public function createOrder(User $user): Order
{
    return Order::create(['user_id' => $user->id]);
}

#[Then('the order should belong to the user')]
public function orderBelongsToUser(Order $order, User $user): void
{
    expect($order->user_id)->toBe($user->id);
}

Reusable Steps

Parameterized Steps

Write generic steps that work across scenarios:

php
// Reusable: Works for any role
#[Given('a user with role {role} exists')]
public function userWithRole(string $role): User
{
    return User::factory()->create(['role' => $role]);
}

// Usage:
// Given a user with role "admin" exists
// Given a user with role "editor" exists
// Given a user with role "viewer" exists

Flexible Patterns

php
// Handles singular and plural
#[Then('the cart should contain {count} item')]
#[Then('the cart should contain {count} items')]
public function cartContains(int $count, Cart $cart): void
{
    expect($cart->items)->toHaveCount($count);
}

Descriptive Patterns

Clear Intent

php
// Good: Clear intent
#[Given('a premium customer exists')]
#[When('the customer purchases an item')]
#[Then('the customer should receive a discount')]

// Avoid: Vague patterns
#[Given('setup user')]
#[When('do action')]
#[Then('check result')]

Domain Language

php
// Good: Uses business terms
#[Given('a farmer with {hectares} hectares of land')]
#[When('they apply for {amount} TL credit')]
#[Then('the application should be approved')]

// Avoid: Technical terms
#[Given('user with attribute land_size = {value}')]
#[When('POST /api/credit with body.amount = {value}')]
#[Then('response.status = 200')]

Laravel Integration

Factories as Given Steps

php
class UserFactory extends Factory
{
    #[Given('a user exists')]
    public function definition(): array
    {
        return [
            'name' => fake()->name(),
            'email' => fake()->email(),
        ];
    }

    #[Given('an admin user exists')]
    public function admin(): static
    {
        return $this->state(['role' => 'admin']);
    }

    #[Given('a user {name} exists')]
    public function withName(string $name): static
    {
        return $this->state(['name' => $name]);
    }
}

Separate Concerns

php
// app/Actions/CreateOrder.php - Business logic
class CreateOrder
{
    #[When('the user creates an order')]
    public function execute(User $user, Cart $cart): Order
    {
        return Order::create([
            'user_id' => $user->id,
            'items' => $cart->items,
            'total' => $cart->total,
        ]);
    }
}

// tests/Assertions/OrderAssertions.php - Test assertions
class OrderAssertions
{
    #[Then('the order should be confirmed')]
    public function confirmed(Order $order): void
    {
        expect($order->status)->toBe('confirmed');
    }

    #[Then('the order total should be ${amount}')]
    public function totalIs(float $amount, Order $order): void
    {
        expect($order->total)->toBe($amount);
    }
}

Multiple Instances

Use Aliases

gherkin
Scenario: Transfer between users
  Given a user "Alice" exists as {sender}
  And the user has 1000 credits as {sender}
  Given a user "Bob" exists as {receiver}
  When sender transfers 500 to receiver
  Then sender should have 500 credits
  And receiver should have 500 credits
php
#[When('{sender} transfers {amount} to {receiver}')]
public function transfer(User $sender, int $amount, User $receiver): void
{
    TransferService::execute($sender, $receiver, $amount);
}

Negative Assertions

Testing Absence

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, Cart $cart): void
{
    expect($cart->contains($product))->toBeFalse();
}

#[Then('no email should be sent')]
public function noEmailSent(): void
{
    Mail::assertNothingSent();
}

Released under the MIT License.