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:
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
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
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
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:
composer dump-autoload --optimizeThen run your BDD tests:
./vendor/bin/pest --bddYou 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.12sUnderstanding the Pattern
Step Distribution
| Step Type | Location | Laravel Pattern |
|---|---|---|
| Given | database/factories/ | Factory states |
| When | app/Actions/ | Action classes |
| Then | tests/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 contextFactory Lazy Creation
Factory steps don't create models immediately. They queue states until a When/Then step:
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:
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"
- Run
composer dump-autoload --optimize - Check that your step class is in an autoloaded directory
- 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