The first time I watched a junior engineer hand-write seven User::create([...]) calls at the top of a feature test, I knew exactly what was about to happen. Two months later, the test was 90 lines, half of it boilerplate, the other half waiting on someone to remember that "test seven" needed a Spanish billing address. Three months in, the test was deleted because nobody could read it.

Factories fix that, but only if you actually use the parts beyond Model::factory()->create(). The Eloquent factory API has grown into a small DSL — states, sequences, relationship helpers, named methods — and the difference between a readable test suite and an unreadable one is usually how much of it your team uses.

Seeders are a separate beast. Factories build data for tests; seeders build data for environments. Confuse the two and you'll either commit fake data into production or spend a week wiring a "realistic dataset" out of files that were meant for one-off tests.

A Factory Definition Is The Default Shape

The php artisan make:factory OrderFactory command drops a class under database/factories/. The definition() method returns the array of column values for one row. That array should describe a sensible, valid, "could exist in production" Order — nothing more.

PHP
namespace Database\Factories;

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

class OrderFactory extends Factory
{
    protected $model = Order::class;

    public function definition(): array
    {
        return [
            'user_id'     => User::factory(),
            'reference'   => strtoupper(fake()->bothify('ORD-#####')),
            'status'      => 'pending',
            'currency'    => 'USD',
            'total_cents' => fake()->numberBetween(2_000, 50_000),
            'placed_at'   => fake()->dateTimeBetween('-30 days', 'now'),
        ];
    }
}

A few rules for definition() that save trouble later:

  1. One row, valid by itself. No counts, no relationships beyond what an Order always has, no business logic.
  2. Use fake() (or the bound Faker generator) for variety. Two consecutive Order::factory()->create() calls should not collide on a unique column.
  3. Lean on nested factories for required relationships. 'user_id' => User::factory() means "create a user if one isn't passed." Pass one in to override.

The everyday calls:

PHP
Order::factory()->create();              // one row, persisted
Order::factory()->make();                // one row, in memory only
Order::factory()->count(10)->create();   // ten rows
Order::factory()->raw();                 // attribute array, no model

States Are Where Tests Become Readable

A "state" is a named variant of the factory — a different shape of the same thing. The two patterns:

PHP
// Inline, one-off
Order::factory()->state(['status' => 'paid'])->create();

// Named methods on the factory class, reusable
public function paid(): self
{
    return $this->state(fn () => [
        'status'   => 'paid',
        'paid_at'  => now(),
    ]);
}

public function cancelled(): self
{
    return $this->state(fn () => [
        'status'       => 'cancelled',
        'cancelled_at' => now(),
        'cancel_reason' => fake()->randomElement(['fraud', 'oos', 'customer']),
    ]);
}

public function pending(): self
{
    return $this->state(fn () => ['status' => 'pending']);
}

Now the test reads as a sentence:

PHP
$order = Order::factory()->paid()->for($manager->team)->create();

That's the difference between a 90-line setup and a one-line setup. States become the vocabulary of your test suite. When a new status shows up in the product, you add one method to the factory and every test that needs it stays a single line.

Relationships: for, has, And The Polymorphic Cousins

Factories model relationships through three helpers — and this is the part most teams under-use.

PHP
// belongsTo: this Order is FOR that team
Order::factory()->for(Team::factory()->state(['plan' => 'pro']))->create();

// hasMany: this Order HAS three line items
Order::factory()->has(LineItem::factory()->count(3))->create();

// nested: a customer with three orders, each with two items
User::factory()
    ->has(
        Order::factory()
            ->count(3)
            ->has(LineItem::factory()->count(2))
    )
    ->create();

The shorter, magic-method aliases work too if you name them right (hasOrders(3), forUser($user)), but the explicit form ages better and reads cleaner in code review.

The case where this matters most is the test that says "a manager can only see their team's orders." Without relationship helpers, that test sets up two teams, two managers, six orders, and twelve line items by hand. With them, it's three lines and the intent is obvious.

Sequence For Variation You Need To See

A Sequence cycles values across rows in a count(...). It's the cleanest way to build data that varies in known ways — exactly what you want for testing filters, ordering, and edge cases.

PHP
use Illuminate\Database\Eloquent\Factories\Sequence;

Order::factory()
    ->count(6)
    ->state(new Sequence(
        ['status' => 'pending'],
        ['status' => 'paid'],
        ['status' => 'shipped'],
        ['status' => 'delivered'],
        ['status' => 'cancelled'],
        ['status' => 'refunded'],
    ))
    ->create();

You now have one of each status, in a single statement. That's the test data for "the dashboard groups orders by status correctly," ready in one line. A Sequence can take a closure too if you need indexed values:

PHP
->state(new Sequence(fn (Sequence $s) => ['placed_at' => now()->subDays($s->index)]))

Combine Sequence with count(), for(), and named states and almost any test scenario becomes a few readable lines.

Diagram showing a factory state graph for an Order — a default pending state in the middle, branches to paid, shipped, delivered, cancelled, and refunded, each labeled with the named factory method, plus a parallel branch showing count(10)->create() fanning out via Sequence into one row per status. Below, the relationship helpers for, has, and nested has-with-count are pictured connecting User, Team, Order, and LineItem.
States plus relationships plus Sequence — three small primitives that turn factories from a one-row helper into a small DSL.

Faker, And Why Locale Matters

fake() returns a Faker generator. The right call makes the difference between data that looks plausible and data that hides bugs.

PHP
fake()->name();                // "Maria Schmidt"
fake()->email();               // unique-ish, but not guaranteed
fake()->unique()->email();     // guaranteed unique within the factory call
fake()->paragraphs(3, true);   // joined string of three paragraphs
fake()->numberBetween(1, 99);
fake()->randomElement(['gold','silver','bronze']);
fake()->dateTimeBetween('-1 year', 'now');
fake()->address();
fake()->phoneNumber();
fake()->uuid();

The locale is set in config/app.php (faker_locale). If your product ships in multiple locales, run a portion of your tests against de_DE or uk_UA data. UTF-8 names break things — collations, search indexes, regex validators. You want to find that out in the test suite, not in production.

A common gotcha: fake()->email() is not unique across rows. For columns with a unique constraint, use fake()->unique()->safeEmail(). The unique() modifier resets per factory build cycle, so you get clean batches.

Seeders Are For Environments, Not Tests

php artisan make:seeder OrderSeeder creates a class with a run() method. Seeders are how you populate a database — local dev, staging, demo environments, sometimes a fresh production for the first time.

PHP
namespace Database\Seeders;

use App\Models\Order;
use App\Models\User;
use Illuminate\Database\Seeder;

class OrderSeeder extends Seeder
{
    public function run(): void
    {
        User::factory()
            ->count(20)
            ->has(Order::factory()->count(5))
            ->create();
    }
}

Then call it directly or wire it through DatabaseSeeder::run():

Bash
php artisan db:seed                     # runs DatabaseSeeder
php artisan db:seed --class=OrderSeeder # runs one
php artisan migrate:fresh --seed        # nuke + reseed for local dev

The trap is mixing seeders into the test path. Don't. Tests should build the data they need with factories, in the test, where it's visible. Seeders are for the developer who just cloned the repo and wants npm run dev to show something.

Three-column comparison of Laravel test data tools. Left column labeled Factories: audience is the test suite; lifetime is per test, torn down with the database transaction; vocabulary includes named state methods like paid(), cancelled(), for($team), has(...), and count() with Sequence. Middle column labeled Seeders: audience is the developer who just cloned the repo plus demo and staging environments; lifetime is until the next migrate:fresh; vocabulary is db:seed, --class=..., migrate:fresh --seed, plus DemoSeeder and StagingSeeder classes. Right column labeled Real PII: rule one is never seed real customer data into staging because internal-only is one wrong env var away from production; rule two is anonymize before importing a production dump, replacing names, emails, phone numbers, addresses, and free-text columns via a php artisan data:anonymize command. A bottom panel shows the 11pm reproduction lane — one tinker line that chains pro team, manager users, and aged pending orders into the exact shape needed to debug a weird production bug locally.
Factories per test, seeders per environment, real PII anonymized before it ever moves — the three-way split that keeps your data tooling honest.

Staging Data Is A Different Problem

The hardest seeding question isn't "how do I make 10,000 orders." It's "how do I get realistic-looking data without leaking real customers." Two rules I've never regretted:

  1. Never seed real PII into staging. Even if the database is internal-only, "internal-only" is one wrong env var away from production traffic.
  2. If you import a production dump, anonymize it before it lands. Replace names, emails, phones, addresses, and any free-text columns that might contain personal data. There's no SQL one-liner for this — write a php artisan data:anonymize command and run it as part of the import script.

For most apps, factories + a DemoSeeder that builds a believable account with believable orders is plenty. Realistic doesn't mean real.

What This Looks Like When You Need It At 11pm

The whole point of investing in factories early is the moment six months later when production has a weird bug and you need to reproduce it locally. With named states, relationship helpers, and one focused seeder, you can spin up the exact shape — a Pro-tier team with two managers, twenty pending orders, three of which are over thirty days old — in one tinker command. Without that, you spend the first hour just inserting rows.

Good factories are how the slow team becomes the fast team. They're also how the test suite stays under a minute as the product grows.