Skip to content

Factory Integration

Laravel Eloquent Factories can serve directly as Given step definitions. This eliminates boilerplate and keeps your test data creation in one place.

Basic Setup

Annotating Definition

The definition() method becomes a basic Given step:

php
use TestFlowLabs\PestTestAttributes\Given;
use Illuminate\Database\Eloquent\Factories\Factory;

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

    #[Given('a user exists')]
    #[Given('bir kullanıcı mevcut')]  // Multi-language support
    public function definition(): array
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'password' => bcrypt('password'),
            'role' => 'user',
        ];
    }
}

Now you can use it in features:

gherkin
Feature: User Management

  Scenario: User exists
    Given a user exists
    Then the user should be in the database

Annotating State Methods

State methods become parameterized Given steps:

php
class UserFactory extends Factory
{
    // ... definition

    #[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 verified user exists')]
    public function verified(): static
    {
        return $this->state(['email_verified_at' => now()]);
    }

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

Usage:

gherkin
Scenario: Create user with specific name
  Given a user "John Doe" exists
  Then the user name should be "John Doe"

Scenario: Verified user login
  Given a verified user exists
  When the user logs in
  Then login should succeed

How It Works

Discovery

When you run pest --bdd, the factory scanner:

  1. Finds factory classes via Composer's classmap
  2. Scans methods for #[Given] attributes
  3. Registers them as factory step definitions

Lazy Creation

Factory steps don't create models immediately. Instead:

gherkin
Scenario: Chained factory states
  Given a user "John" exists          # Queue: withName("John")
  And a verified user exists          # Queue: verified()
  And a user with email "j@test.com" exists  # Queue: withEmail("j@test.com")
  When I do something                 # CREATE NOW!

When transitioning from Given to When/Then, all queued states are applied and the model is created:

php
// Internally executed as:
User::factory()
    ->withName("John")
    ->verified()
    ->withEmail("j@test.com")
    ->create();

Context Storage

After creation, the model is stored in the scenario context:

  • By type: Available to any step requesting User $user
  • By alias: If using as {alias} syntax

Complete Factory Example

php
<?php

namespace Database\Factories;

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

/**
 * @extends Factory<User>
 */
class UserFactory extends Factory
{
    protected $model = User::class;

    /**
     * Default user definition.
     */
    #[Given('a user exists')]
    #[Given('bir kullanıcı mevcut')]
    public function definition(): array
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'password' => bcrypt('password'),
            'role' => 'user',
            'email_verified_at' => now(),
            'is_active' => true,
        ];
    }

    /**
     * User with specific name.
     */
    #[Given('a user {name} exists')]
    #[Given('kullanıcı {name} mevcut')]
    public function withName(string $name): static
    {
        return $this->state(['name' => $name]);
    }

    /**
     * User with specific email.
     */
    #[Given('a user with email {email} exists')]
    public function withEmail(string $email): static
    {
        return $this->state(['email' => $email]);
    }

    /**
     * Admin user.
     */
    #[Given('an admin user exists')]
    #[Given('bir admin kullanıcı mevcut')]
    public function admin(): static
    {
        return $this->state(['role' => 'admin']);
    }

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

    /**
     * Inactive user.
     */
    #[Given('an inactive user exists')]
    public function inactive(): static
    {
        return $this->state(['is_active' => false]);
    }

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

    /**
     * User created on specific date.
     */
    #[Given('a user created {days} days ago exists')]
    public function createdDaysAgo(int $days): static
    {
        return $this->state([
            'created_at' => now()->subDays($days),
        ]);
    }

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

Feature File Examples

Basic User Creation

gherkin
Feature: User Registration

  Scenario: Basic user
    Given a user exists
    Then the user should have role "user"

  Scenario: Named user
    Given a user "Jane Smith" exists
    Then the user name should be "Jane Smith"

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

Chained States

gherkin
Feature: User Roles and Status

  Scenario: Active admin
    Given an admin user exists
    And a user "John Admin" exists
    When I check the user role
    Then the user should be an admin named "John Admin"

  Scenario: Inactive moderator
    Given a moderator user exists
    And an inactive user exists
    When I try to login
    Then login should fail with "Account inactive"

  Scenario: Old unverified user
    Given a user created 30 days ago exists
    And an unverified user exists
    When the cleanup job runs
    Then the user should be deleted

Then Steps for Factory-Created Models

Create assertion steps that receive the factory-created model:

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

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

    #[Then('the user should be active')]
    public function assertActive(User $user): void
    {
        expect($user->is_active)->toBeTrue();
    }

    #[Then('the user should be verified')]
    public function assertVerified(User $user): void
    {
        expect($user->email_verified_at)->not->toBeNull();
    }

    #[Then('the user should be in the database')]
    public function assertInDatabase(User $user): void
    {
        $this->assertDatabaseHas('users', ['id' => $user->id]);
    }
}

Using Relationships

php
class PostFactory extends Factory
{
    #[Given('a post exists')]
    public function definition(): array
    {
        return [
            'title' => $this->faker->sentence(),
            'content' => $this->faker->paragraphs(3, true),
            'user_id' => User::factory(),  // Creates a user
        ];
    }

    #[Given('a post by the user exists')]
    public function byUser(User $user): static
    {
        // $user is injected from context!
        return $this->state(['user_id' => $user->id]);
    }

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

Usage:

gherkin
Scenario: User's published post
  Given a user "Author" exists
  And a post by the user exists
  And a published post exists
  When I view the post
  Then I should see the author "Author"

Sequence Relationships

php
class CommentFactory extends Factory
{
    #[Given('the post has {count} comments')]
    public function forPost(Post $post, int $count): static
    {
        // This creates multiple comments
        return $this->count($count)->state(['post_id' => $post->id]);
    }
}

Best Practices

Keep Factories Focused

Each factory should only define steps for its own model:

php
// Good: UserFactory defines user steps
class UserFactory extends Factory
{
    #[Given('a user exists')]
    public function definition() { }
}

// Avoid: UserFactory defining post steps
class UserFactory extends Factory
{
    #[Given('a post exists')]  // Wrong place!
    public function createPost() { }
}

Use Descriptive State Names

php
// Good: Clear intent
#[Given('an admin user exists')]
public function admin(): static { }

#[Given('a user with expired subscription exists')]
public function withExpiredSubscription(): static { }

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

Document Complex States

php
/**
 * Creates a user with a complete profile.
 * Includes: avatar, bio, social links, preferences.
 */
#[Given('a user with complete profile exists')]
public function withCompleteProfile(): static
{
    return $this->state([
        'avatar' => $this->faker->imageUrl(),
        'bio' => $this->faker->paragraph(),
        'social_links' => ['twitter' => '@user'],
        'preferences' => ['theme' => 'dark'],
    ]);
}

Container Integration

Factory classes are resolved through Laravel's Service Container, just like any other class. This means factories with constructor dependencies work seamlessly:

php
class OrderFactory extends Factory
{
    public function __construct(
        private PricingService $pricing,
    ) {
        parent::__construct();
    }

    #[Given('an order with calculated price exists')]
    public function withCalculatedPrice(): static
    {
        $price = $this->pricing->getBasePrice();

        return $this->state(['price' => $price]);
    }
}

See Service Container Integration for more details on DI support.

Released under the MIT License.