Skip to content

Laravel Integration ​

Pest BDD provides first-class Laravel support, making it natural to use BDD with Laravel applications. The integration focuses on leveraging existing Laravel patterns, especially Eloquent Factories.

Why Laravel Integration? ​

Laravel developers already have powerful tools for testing:

  • Factories for creating test data
  • Actions for encapsulating business logic
  • Database transactions for test isolation
  • HTTP testing for API verification

Pest BDD builds on these patterns rather than replacing them.

Factory-First BDD ​

The core philosophy is simple: your factories ARE your Given steps.

Instead of:

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

With factory integration:

php
// Factory IS the step definition
class UserFactory extends Factory
{
    #[Given('a user {name} exists')]
    public function withName(string $name): static
    {
        return $this->state(['name' => $name]);
    }
}

Key Features ​

1. Factory State Methods as Steps ​

Annotate factory state methods with Given attributes:

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

    #[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]);
    }
}

2. Lazy Model Creation ​

Models aren't created immediately. Instead, states are chained and models are created when transitioning from Given to When/Then:

gherkin
Scenario: Admin with specific name
  Given a user "John" exists        # Chain: withName("John")
  And an admin user exists          # Chain: admin()
  When I check permissions          # CREATE! User::factory()->withName("John")->admin()->create()
  Then the user should be admin

This enables:

  • State composition
  • Efficient database operations
  • Natural Gherkin flow

3. Auto-Injection of Models ​

Created models are automatically available in subsequent steps:

php
#[When('I check permissions')]
public function checkPermissions(User $user): void
{
    // $user is automatically injected - created from Given steps
    $this->response = $this->actingAs($user)->get('/admin');
}

#[Then('the user should be admin')]
public function shouldBeAdmin(User $user): void
{
    expect($user->role)->toBe('admin');
}

4. Multiple Instances with Aliases ​

Create multiple instances of the same model using aliases:

gherkin
Scenario: Compare two users
  Given a user "John" exists as {regularUser}
  And a user "Jane" exists as {adminUser}
  And an admin user exists as {adminUser}
  When I compare users
  Then regularUser should not be admin
  And adminUser should be admin
php
#[Then('regularUser should not be admin')]
public function regularUserNotAdmin(User $regularUser): void
{
    expect($regularUser->role)->not->toBe('admin');
}

#[Then('adminUser should be admin')]
public function adminUserIsAdmin(User $adminUser): void
{
    expect($adminUser->role)->toBe('admin');
}

Getting Started ​

1. Add Attributes to Your Factory ​

php
// database/factories/UserFactory.php
namespace Database\Factories;

use App\Models\User;
use TestFlowLabs\PestTestAttributes\Given;
use Illuminate\Database\Eloquent\Factories\Factory;

class UserFactory extends Factory
{
    protected $model = User::class;

    #[Given('a user exists')]
    public function definition(): array
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'password' => bcrypt('password'),
        ];
    }

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

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

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

2. Write Your Feature ​

gherkin
# tests/Behaviors/user-profile.feature
Feature: User Profile

  Scenario: View own profile
    Given a user "John Doe" exists
    When the user visits their profile
    Then they should see "John Doe"

  Scenario: Admin can view all profiles
    Given an admin user exists
    When the admin visits the users list
    Then they should see all users

3. Add When/Then Steps ​

php
// app/Actions/Profile/VisitProfile.php - When step (production)
class VisitProfile
{
    #[When('the user visits their profile')]
    public function execute(User $user): TestResponse
    {
        return $this->actingAs($user)->get('/profile');
    }
}

// tests/Assertions/Profile/ProfileAssertions.php - Then step
class ProfileAssertions
{
    #[Then('they should see {name}')]
    public function shouldSeeName(string $name, TestResponse $response): void
    {
        $response->assertSee($name);
    }
}

4. Run Tests ​

bash
pest --bdd

Sections ​

Explore detailed Laravel integration topics:

Best Practices ​

Organize Steps by Location ​

database/
└── factories/                      # Given steps - Factory'ler
    ├── UserFactory.php
    └── OrderFactory.php

app/
└── Actions/                        # When steps - Production Actions
    ├── User/
    │   └── CreateUser.php
    └── Order/
        └── PlaceOrder.php

tests/
├── Assertions/                     # Then steps - Assertion'lar
│   ├── User/
│   │   └── UserAssertions.php
│   └── Order/
│       └── OrderAssertions.php
└── Behaviors/                      # Feature files
    ├── authentication.feature
    └── ordering.feature

Use Actions for When Steps ​

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

Use Assertion Classes for Then Steps ​

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

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

Released under the MIT License.