Skip to content

Quick Start

This guide will get you up and running with Pest BDD in under 5 minutes. We'll create a real-world user registration feature using Laravel's native patterns.

Step 1: Create a Feature File

Create your first feature file at tests/Behaviors/user-registration.feature:

gherkin
Feature: User Registration
  As a visitor
  I want to register an account
  So that I can access the application

  Scenario: Successful registration
    Given a user "John Doe" exists
    When I register with email "jane@example.com"
    Then the user should exist in the database
    And the user should have email "jane@example.com"

  Scenario: Admin user creation
    Given an admin user exists
    Then the user should have role "admin"

Step 2: Create Step Definitions

In Pest BDD, steps are distributed across Laravel structures:

Given Steps → Factory

Add step attributes to your factory at database/factories/UserFactory.php:

php
<?php

namespace Database\Factories;

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

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

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

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

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

When Steps → Action

Create an action at app/Actions/User/RegisterUser.php:

php
<?php

namespace App\Actions\User;

use App\Models\User;
use TestFlowLabs\PestTestAttributes\When;

class RegisterUser
{
    #[When('I register with email {email}')]
    public function __invoke(string $email): User
    {
        return User::create([
            'name' => 'New User',
            'email' => $email,
            'password' => bcrypt('password'),
            'role' => 'user',
        ]);
    }
}

Then Steps → Assertion Class

Create assertions at tests/Assertions/UserAssertions.php:

php
<?php

namespace Tests\Assertions;

use App\Models\User;
use TestFlowLabs\PestTestAttributes\Then;

class UserAssertions
{
    #[Then('the user should exist in the database')]
    public function shouldExist(User $user): void
    {
        expect(User::find($user->id))->not->toBeNull();
    }

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

    #[Then('the user should have role {role}')]
    public function shouldHaveRole(string $role, User $user): void
    {
        expect($user->role)->toBe($role);
    }
}

Auto-Injection

Notice how User $user is automatically injected into Then steps. Pest BDD tracks objects returned from previous steps and injects them by type.

Step 3: Run the Tests

First, ensure your autoloader is optimized:

bash
composer dump-autoload --optimize

Then run your BDD tests:

bash
./vendor/bin/pest --bdd

You should see output like this:

   PASS  Feature: User Registration
   ✓ Scenario: Successful registration
     ✓ Given a user "John Doe" exists
     ✓ When I register with email "jane@example.com"
     ✓ Then the user should exist in the database
     ✓ And the user should have email "jane@example.com"
   ✓ Scenario: Admin user creation
     ✓ Given an admin user exists
     ✓ Then the user should have role "admin"

  Tests:    2 passed
  Duration: 0.12s

Understanding the Pattern

Step Distribution

Step TypeLocationLaravel Pattern
Givendatabase/factories/Factory states
Whenapp/Actions/Action classes
Thentests/Assertions/Test assertions

Auto-Injection Flow

Given a user "John Doe" exists     → Returns User → Stored in context
When I register with email "..."   → Returns User → Stored in context
Then the user should have email    → User $user injected from context

Factory Lazy Creation

Factory steps don't create models immediately. They queue states until a When/Then step:

gherkin
Given a user "John" exists      # Queue: withName("John")
And an admin user exists        # Queue: admin()
When I do something             # NOW creates: User::factory()->withName("John")->admin()->create()

Alternative: Simple Example

For non-Laravel projects or simpler cases, all steps can be in one class:

php
class CalculatorSteps
{
    private int $result = 0;

    #[Given('I have number {n}')]
    public function haveNumber(int $n): void
    {
        $this->result = $n;
    }

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

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

Common Issues

"No step definition found"

  1. Run composer dump-autoload --optimize
  2. Check that your step class is in an autoloaded directory
  3. Verify the pattern matches the step text exactly

Model Not Injected

If a Model isn't being injected into Then steps:

  • Ensure the Given/When step returns the model
  • Check the type hint matches exactly

Released under the MIT License.