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 PSR-4 namespace scanning
  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.