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:
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:
Feature: User Management
Scenario: User exists
Given a user exists
Then the user should be in the databaseAnnotating State Methods
State methods become parameterized Given steps:
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:
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 succeedHow It Works
Discovery
When you run pest --bdd, the factory scanner:
- Finds factory classes via Composer's classmap
- Scans methods for
#[Given]attributes - Registers them as factory step definitions
Lazy Creation
Factory steps don't create models immediately. Instead:
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:
// 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
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
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
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 deletedThen Steps for Factory-Created Models
Create assertion steps that receive the factory-created model:
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]);
}
}Related Factories
Using Relationships
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:
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
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:
// 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
// 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
/**
* 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:
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.