0x55aa
← Back to Blog

Laravel Factories & Seeders: Fake Data That Looks Real 🎭

8 min read

Laravel Factories & Seeders: Fake Data That Looks Real 🎭

Ever spent 2 hours manually clicking through your app to create test data? Yeah, I've been there. Then you reset the database and have to do it all over again. Fun times! 😅

Let me show you how Model Factories and Seeders can save you from this tedious nightmare. This is EXACTLY how we handle test data in production systems at Cubet Techno Labs.

What's the Deal with Factories? 🏭

Think of factories as blueprints for creating fake (but realistic) data. Instead of writing the same User::create() code 47 times, you define it once and generate as many users as you need.

The old painful way:

// Somewhere in your test or seed file... again... and again...
User::create([
    'name' => 'John Doe',
    'email' => '[email protected]',
    'password' => bcrypt('password'),
    'email_verified_at' => now(),
]);

The factory way:

User::factory()->create();
// Done! One realistic user with all fields filled.

User::factory()->count(50)->create();
// Need 50 users? No problem!

As a Technical Lead, I've learned that good test data = confident deployments. You can't test what you can't see!

Real Talk: Why I Started Using Factories 💬

In production systems I've built, we needed to:

  • Test features with hundreds of records (not just 3)
  • Demo the app to clients with realistic-looking data
  • Onboard new devs without them spending a day clicking buttons
  • Run automated tests that need fresh data every time

Factories solved ALL of this. One command, boom - entire database populated with realistic data. 🎯

Building Your First Factory 🔨

Let's say you're building a blog. Here's how to create factories for real-world scenarios:

php artisan make:factory PostFactory

Basic Factory (database/factories/PostFactory.php):

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition()
    {
        return [
            'user_id' => User::factory(),
            'title' => fake()->sentence(),
            'slug' => fake()->slug(),
            'content' => fake()->paragraphs(3, true),
            'status' => 'published',
            'published_at' => fake()->dateTimeBetween('-1 year', 'now'),
            'views' => fake()->numberBetween(0, 10000),
        ];
    }
}

Pro tip: That fake() helper is GOLD. It uses Faker library to generate realistic data. Names, emails, paragraphs, dates - you name it!

Factory States: The Secret Sauce 🌶️

Here's where it gets powerful. What if you need different types of posts? Published, drafts, featured?

class PostFactory extends Factory
{
    public function definition()
    {
        return [
            'user_id' => User::factory(),
            'title' => fake()->sentence(),
            'content' => fake()->paragraphs(3, true),
            'status' => 'draft',
            'published_at' => null,
        ];
    }

    // State for published posts
    public function published()
    {
        return $this->state(fn (array $attributes) => [
            'status' => 'published',
            'published_at' => fake()->dateTimeBetween('-1 year', 'now'),
        ]);
    }

    // State for featured posts
    public function featured()
    {
        return $this->state(fn (array $attributes) => [
            'is_featured' => true,
            'views' => fake()->numberBetween(5000, 50000),
        ]);
    }

    // State for viral posts
    public function viral()
    {
        return $this->state(fn (array $attributes) => [
            'is_featured' => true,
            'views' => fake()->numberBetween(100000, 1000000),
            'published_at' => fake()->dateTimeBetween('-1 month', 'now'),
        ]);
    }
}

Now watch the magic:

Post::factory()->published()->create();
Post::factory()->featured()->create();
Post::factory()->viral()->count(5)->create();

// Chain them!
Post::factory()->published()->featured()->create();

A pattern that saved us in a real project: We needed to test pagination with different content types. Instead of manually creating 100 posts, I just ran:

Post::factory()->published()->count(75)->create();
Post::factory()->count(25)->create(); // drafts

Two lines. 100 realistic posts. Ready to test. 🚀

Relationships: The Tricky Part (Made Easy) 🔗

The challenge: Creating related data that makes sense.

Bad way (creates orphaned data):

$user = User::factory()->create();
$post = Post::factory()->create(); // Has a DIFFERENT random user!

Good way (explicit relationships):

$user = User::factory()
    ->has(Post::factory()->count(5))
    ->create();
// One user with 5 posts!

Even better way (using relationships):

$user = User::factory()
    ->has(Post::factory()->published()->count(10))
    ->has(Post::factory()->count(5)) // 5 drafts
    ->create();
// One user with 15 posts (10 published, 5 drafts)

For the reverse (belongsTo):

Post::factory()
    ->for(User::factory()->state(['name' => 'John Doe']))
    ->create();
// Post belongs to a specific user

In production systems I've built, we had complex relationships: Users -> Posts -> Comments -> Likes. Creating test data was a nightmare UNTIL we set up factories properly:

User::factory()
    ->count(10)
    ->has(
        Post::factory()
            ->count(3)
            ->has(Comment::factory()->count(5))
    )
    ->create();

// 10 users, each with 3 posts, each post with 5 comments
// That's 10 users + 30 posts + 150 comments = 190 records
// Created in ONE LINE!

Mind = blown. 🤯

Seeders: Factories on Steroids 💪

Factories create data. Seeders orchestrate it. Here's how to set up a realistic database:

php artisan make:seeder DatabaseSeeder

Simple Seeder (database/seeders/DatabaseSeeder.php):

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Post;
use App\Models\Category;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // Create an admin user (always the same)
        $admin = User::factory()->create([
            'name' => 'Admin User',
            'email' => '[email protected]',
            'role' => 'admin',
        ]);

        // Create some categories
        $categories = Category::factory()->count(5)->create();

        // Create regular users with posts
        User::factory()
            ->count(20)
            ->has(
                Post::factory()
                    ->count(3)
                    ->state(function (array $attributes, User $user) use ($categories) {
                        return [
                            'category_id' => $categories->random()->id,
                        ];
                    })
            )
            ->create();

        // Create some featured posts
        Post::factory()
            ->published()
            ->featured()
            ->count(10)
            ->create();
    }
}

Run it:

php artisan db:seed
# Or if you're refreshing everything:
php artisan migrate:fresh --seed

Pro Tips from the Trenches 💡

1. Use Sequences for Variety

Need data with specific patterns?

$users = User::factory()
    ->count(10)
    ->sequence(
        ['role' => 'admin'],
        ['role' => 'editor'],
        ['role' => 'user'],
    )
    ->create();
// Creates users cycling through the roles

2. Create Realistic Emails

public function definition()
{
    $name = fake()->name();
    return [
        'name' => $name,
        'email' => Str::slug($name) . fake()->unique()->numberBetween(1, 999) . '@example.com',
        // [email protected] - looks realistic!
    ];
}

3. Separate Seeders for Different Environments

// LocalSeeder.php - Lots of test data
public function run()
{
    User::factory()->count(100)->create();
}

// ProductionSeeder.php - Just essentials
public function run()
{
    // Only create admin and default settings
}

Then call specific seeders:

php artisan db:seed --class=LocalSeeder

4. Use Factories in Tests

public function test_user_can_create_post()
{
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/posts', [
            'title' => 'Test Post',
            'content' => 'Test Content',
        ])
        ->assertStatus(201);

    $this->assertDatabaseHas('posts', [
        'title' => 'Test Post',
        'user_id' => $user->id,
    ]);
}

Clean, readable, maintainable. That's the Laravel way! ✨

The "I Wish I Knew This Earlier" Moment 😅

You can call factories from tinker for quick debugging:

php artisan tinker
>>> User::factory()->count(5)->create()
>>> Post::factory()->published()->create(['title' => 'Debug Post'])

This saved me countless times when I just needed ONE more test record to check something.

Real-World Factory Recipe 🍳

Here's the exact setup I use for e-commerce projects:

// UserFactory - Customers with different states
User::factory()
    ->count(50)
    ->create();

User::factory()
    ->state(['is_premium' => true])
    ->count(10)
    ->create();

// ProductFactory - Various products
Product::factory()
    ->count(100)
    ->create();

Product::factory()
    ->state(['stock' => 0])
    ->count(10)
    ->create(); // Out of stock products

// OrderFactory - Orders with items
Order::factory()
    ->count(200)
    ->has(OrderItem::factory()->count(3))
    ->create();

Run time: ~5 seconds. Manual creation time: ~5 hours. You do the math! 📊

Common Gotchas ⚠️

1. Don't Forget to Define Relationships in Models

Factories use your model relationships. If User::posts() doesn't exist, ->has(Post::factory()) will fail.

2. Unique Constraints

'email' => fake()->unique()->safeEmail(),
// That unique() call is IMPORTANT!

3. Foreign Key Constraints

Create parent records first:

$user = User::factory()->create();
Post::factory()->for($user)->count(5)->create();

4. Heavy Seeders

If your seeder takes forever, break it into smaller ones:

php artisan make:seeder UserSeeder
php artisan make:seeder PostSeeder
php artisan make:seeder CategorySeeder

The Developer Experience Win 🎯

Here's what changed after implementing proper factories:

Before:

  • New dev joins → Spends 3 hours manually creating test data
  • Reset database → Lose all test data, start clicking again
  • Test pagination → Not enough data to test properly

After:

  • New dev joins → php artisan migrate:fresh --seed → Coffee break ☕
  • Reset database → Run seed command → Done
  • Test pagination → 10,000 realistic records in 30 seconds

The productivity gain is MASSIVE. Plus, everyone's local environment looks the same!

Your Factory Checklist ✅

  • Create factories for all models (at least the main ones)
  • Add states for different scenarios (published, draft, featured, etc.)
  • Set up relationships properly (has/for)
  • Create a comprehensive DatabaseSeeder
  • Test your factories (yes, really!)
  • Document any special states or uses
  • Use factories in tests

Bonus: Faker Favorites 🎁

Some of my most-used Faker methods:

fake()->name()                    // John Doe
fake()->safeEmail()              // [email protected]
fake()->sentence()               // Lorem ipsum dolor sit amet.
fake()->paragraphs(3, true)      // 3 paragraphs as string
fake()->numberBetween(1, 100)    // Random number
fake()->dateTimeBetween('-1 year', 'now')
fake()->randomElement(['a', 'b', 'c'])
fake()->boolean(70)              // 70% chance of true
fake()->imageUrl(640, 480)       // Placeholder image

Check the Faker docs for the full list!

The Bottom Line

Factories and Seeders aren't just nice-to-haves - they're essential for modern Laravel development. They help you:

  • Test with realistic data volumes
  • Onboard developers faster
  • Create consistent demo environments
  • Write better tests
  • Save HOURS of manual work

In production systems I've built, proper factories have saved literally hundreds of developer-hours across the team. They're a one-time investment that pays dividends forever.


Want to level up your Laravel game? Connect with me on LinkedIn - I share production tips weekly!

Like these real-world Laravel patterns? Star this blog on GitHub for more!

Now go generate some data! 🎭✨