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:
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:
#[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 Hint | Generated Regex | Matches |
|---|---|---|
string | "[^"]*" | "quoted text" |
int | -?\d+ | 42, -5 |
float | -?\d*\.?\d+ | 3.14, .5, -2.5 |
bool | true|false|yes|no|1|0 | true, yes, 0 |
String Parameters
Strings must be quoted in the Gherkin:
Given a user "John Doe" exists#[Given('a user {name} exists')]
public function userExists(string $name): void
{
// $name = "John Doe"
}Integer Parameters
Integers are bare numbers:
When I add 5 items to the cart#[When('I add {count} items to the cart')]
public function addItems(int $count): void
{
// $count = 5
}Float Parameters
Floats support decimals:
Then the price should be 99.99#[Then('the price should be {price}')]
public function priceShouldBe(float $price): void
{
// $price = 99.99
}Boolean Parameters
Booleans accept multiple formats:
Given notifications are true
Given notifications are yes
Given notifications are 1#[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:
// 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:
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:
#[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:
#[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:
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 ThenYou only need to define the step once with its base type:
#[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:
#[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:
#[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:
#[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:
#[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.phpOrganization Summary
| Step Type | TDD Equivalent | Location | Description |
|---|---|---|---|
| Given | Arrange | database/factories/ | Laravel Factory class'ları |
| When | Act | app/Actions/ | Production Action class'ları |
| Then | Assert | tests/Assertions/ | Test assertion class'ları |
Common Patterns
Reusable Steps
Create generic steps that work across scenarios:
#[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
#[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
#[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();
}