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

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. Check that your step class is in an autoloaded directory
  2. Verify the pattern matches the step text exactly
  3. Ensure the namespace matches your composer.json autoload configuration

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.