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.