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 adminThis 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 informationComplex 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 permissionsProgressive 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 availableScenario 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 archivedBest 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 existsDocument 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,
]);
}