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" existsFlexible 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 creditsphp
#[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();
}