Skip to content

Factory States ​

Factory states allow you to define variations of your models. In Pest BDD, state methods become Given steps that can be chained together naturally.

State Method Basics ​

A state method returns static and uses $this->state():

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

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

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

Chaining States ​

States can be chained in Gherkin using And:

gherkin
Scenario: Premium admin user
  Given a user exists
  And an admin user exists
  And a premium user exists
  When I check the user
  Then the user should be a premium admin

This is equivalent to:

php
User::factory()
    ->admin()
    ->premium()
    ->create();

Order Matters ​

Later states override earlier ones:

gherkin
Scenario: State ordering
  Given a user with role "user" exists
  And an admin user exists              # role becomes "admin"
  # Final: role = "admin"
gherkin
Scenario: Opposite ordering
  Given an admin user exists            # role = "admin"
  And a user with role "user" exists    # role becomes "user"
  # Final: role = "user"

Parameterized States ​

States can accept parameters extracted from step text:

php
class UserFactory extends Factory
{
    #[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('a user created {days} days ago exists')]
    public function createdDaysAgo(int $days): static
    {
        return $this->state([
            'created_at' => now()->subDays($days),
        ]);
    }

    #[Given('a user with {count} credits exists')]
    public function withCredits(int $count): static
    {
        return $this->state(['credits' => $count]);
    }

    #[Given('a user aged {age} exists')]
    public function withAge(int $age): static
    {
        return $this->state([
            'birth_date' => now()->subYears($age),
        ]);
    }
}

Usage:

gherkin
Scenario: Parameterized states
  Given a user "John Smith" exists
  And a user with email "john@example.com" exists
  And a user created 30 days ago exists
  And a user with 100 credits exists
  When I view the user profile
  Then I should see all the correct information

Complex State Logic ​

Conditional States ​

php
class OrderFactory extends Factory
{
    #[Given('an order with status {status} exists')]
    public function withStatus(string $status): static
    {
        $state = ['status' => $status];

        // Add appropriate timestamps based on status
        if ($status === 'completed') {
            $state['completed_at'] = now();
        } elseif ($status === 'cancelled') {
            $state['cancelled_at'] = now();
        } elseif ($status === 'shipped') {
            $state['shipped_at'] = now();
        }

        return $this->state($state);
    }
}

States with Relationships ​

php
class PostFactory extends Factory
{
    /**
     * Post with a specific number of comments.
     */
    #[Given('a post with {count} comments exists')]
    public function withComments(int $count): static
    {
        return $this->has(
            Comment::factory()->count($count),
            'comments'
        );
    }

    /**
     * Post with tags.
     */
    #[Given('a post tagged with {tags} exists')]
    public function withTags(string $tags): static
    {
        return $this->afterCreating(function (Post $post) use ($tags) {
            $tagNames = array_map('trim', explode(',', $tags));
            foreach ($tagNames as $tagName) {
                $tag = Tag::firstOrCreate(['name' => $tagName]);
                $post->tags()->attach($tag);
            }
        });
    }

    /**
     * Featured post.
     */
    #[Given('a featured post exists')]
    public function featured(): static
    {
        return $this->state([
            'is_featured' => true,
            'featured_at' => now(),
        ]);
    }
}

States Using Faker ​

php
class ProductFactory extends Factory
{
    #[Given('an expensive product exists')]
    public function expensive(): static
    {
        return $this->state([
            'price' => $this->faker->randomFloat(2, 500, 2000),
        ]);
    }

    #[Given('a cheap product exists')]
    public function cheap(): static
    {
        return $this->state([
            'price' => $this->faker->randomFloat(2, 1, 50),
        ]);
    }

    #[Given('a product with long description exists')]
    public function withLongDescription(): static
    {
        return $this->state([
            'description' => $this->faker->paragraphs(5, true),
        ]);
    }
}

State Composition Patterns ​

Base + Modification Pattern ​

gherkin
Scenario: Premium admin with specific name
  Given a user "Jane Admin" exists    # Base: name set
  And an admin user exists            # Modification: role set
  And a premium user exists           # Modification: tier set
  When I check permissions
  Then Jane should have all permissions

Progressive Enhancement ​

gherkin
Scenario: Fully configured user
  Given a user exists                 # Base user
  And a verified user exists          # Add verification
  And a user with complete profile exists  # Add profile
  And a user with 2FA enabled exists  # Add security
  When I view the account
  Then all features should be available

Scenario Outline with States ​

gherkin
Scenario Outline: Different user types
  Given a user "<name>" exists
  And <type> user exists
  When I check the dashboard
  Then I should see <permissions> permissions

  Examples:
    | name    | type       | permissions |
    | John    | an admin   | all         |
    | Jane    | a moderator| some        |
    | Bob     | a regular  | basic       |

Complete Example ​

php
<?php

namespace Database\Factories;

use App\Models\User;
use App\Models\Team;
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'),
            'role' => 'user',
            'status' => 'active',
            'email_verified_at' => now(),
        ];
    }

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

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

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

    // Status states
    #[Given('an inactive user exists')]
    public function inactive(): static
    {
        return $this->state(['status' => 'inactive']);
    }

    #[Given('a suspended user exists')]
    public function suspended(): static
    {
        return $this->state([
            'status' => 'suspended',
            'suspended_at' => now(),
        ]);
    }

    // Verification states
    #[Given('an unverified user exists')]
    public function unverified(): static
    {
        return $this->state(['email_verified_at' => null]);
    }

    // Team relationship
    #[Given('a user in team {teamName} exists')]
    public function inTeam(string $teamName): static
    {
        return $this->afterCreating(function (User $user) use ($teamName) {
            $team = Team::firstOrCreate(['name' => $teamName]);
            $user->teams()->attach($team);
        });
    }

    // Time-based states
    #[Given('a user registered {days} days ago exists')]
    public function registeredDaysAgo(int $days): static
    {
        return $this->state([
            'created_at' => now()->subDays($days),
        ]);
    }

    // Complex states
    #[Given('a user with complete profile exists')]
    public function withCompleteProfile(): static
    {
        return $this->state([
            'avatar' => $this->faker->imageUrl(200, 200),
            'bio' => $this->faker->paragraph(),
            'website' => $this->faker->url(),
            'location' => $this->faker->city(),
            'phone' => $this->faker->phoneNumber(),
        ]);
    }
}
gherkin
Feature: User Management
  As an administrator
  I want to manage users
  So that I can control access to the system

  Scenario: View active admin
    Given a user "John Admin" exists
    And an admin user exists
    When I view the users list
    Then I should see "John Admin" as admin

  Scenario: Suspended user cannot login
    Given a user "Jane Suspended" exists
    And a suspended user exists
    When the user tries to login
    Then login should fail with "Account suspended"

  Scenario: Unverified user prompt
    Given a user "Bob Unverified" exists
    And an unverified user exists
    When the user logs in
    Then they should see verification prompt

  Scenario: Team member access
    Given a user "Alice Team" exists
    And a user in team "Engineering" exists
    When the user accesses team resources
    Then they should see Engineering resources

  Scenario: Old inactive user cleanup
    Given a user "Old User" exists
    And an inactive user exists
    And a user registered 365 days ago exists
    When the cleanup job runs
    Then the user should be archived

Best Practices ​

Name States Clearly ​

php
// Good: Intent is clear
#[Given('a premium subscriber exists')]
public function premiumSubscriber(): static { }

// Avoid: Generic names
#[Given('a special user exists')]
public function special(): static { }

Keep States Atomic ​

Each state should do one thing:

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

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

// Can be combined:
// Given an admin user exists
// And a premium user exists

Document Complex States ​

php
/**
 * Creates a user with an expired trial.
 * - Trial started 15 days ago
 * - Trial period is 14 days
 * - User should see upgrade prompt
 */
#[Given('a user with expired trial exists')]
public function withExpiredTrial(): static
{
    return $this->state([
        'trial_started_at' => now()->subDays(15),
        'trial_period_days' => 14,
    ]);
}

Released under the MIT License.