Laravel Magazine
Better Test Data with Laravel Model Factories and States

Better Test Data with Laravel Model Factories and States

Eric Van Johnson ·

Model factories are the fastest way to build test data in Laravel, but most people stop at the basics. States, relationships, and sequences turn factories into a tiny domain language for describing exactly the scenario a test needs. Here are the pieces worth mastering.

Start With a Good Base Definition

The factory's definition() should return a valid, realistic record. Keep it minimal but complete, so every generated model is usable without extra setup.

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class InvoiceFactory extends Factory
{
    public function definition(): array
    {
        return [
            'number' => 'INV-' . $this->faker->unique()->numberBetween(1000, 9999),
            'amount_cents' => $this->faker->numberBetween(500, 500000),
            'status' => 'pending',
        ];
    }
}

States Describe Variations

A state is a named tweak to the base definition. Instead of overriding attributes inline all over your tests, give the meaningful variations names.

public function paid(): static
{
    return $this->state(fn (array $attributes) => [
        'status' => 'paid',
        'paid_at' => now(),
    ]);
}

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

Now your tests read like sentences:

$paid = Invoice::factory()->paid()->create();
$overdue = Invoice::factory()->count(3)->void()->create();

Relationships in One Line

Factories compose. You can attach related models directly, and Laravel wires up the foreign keys for you.

// A customer with 5 paid invoices
$customer = Customer::factory()
    ->has(Invoice::factory()->count(5)->paid())
    ->create();

// From the other direction, using the magic for* method
$invoice = Invoice::factory()
    ->for(Customer::factory()->state(['name' => 'PHP Architect']))
    ->create();

Sequences for Varying Data

When you need a set of records that differ in a controlled way, a sequence cycles through values as models are created.

use Illuminate\Database\Eloquent\Factories\Sequence;

$invoices = Invoice::factory()
    ->count(6)
    ->state(new Sequence(
        ['status' => 'paid'],
        ['status' => 'pending'],
    ))
    ->create();

That produces three paid and three pending invoices, alternating. Sequences are ideal for testing filters and reports where you need a known mix.

Run Logic After Creation

The afterCreating hook lets a factory perform side effects once the model exists, which is handy for things that cannot be set as plain attributes.

public function configure(): static
{
    return $this->afterCreating(function (Invoice $invoice) {
        $invoice->recalculateTotals();
    });
}

A Quick Tip: make() vs create()

Use create() when the test needs the record in the database, and make() when you only need an in-memory instance. make() skips the database entirely, which keeps unit tests that do not touch persistence fast.

Well-designed factories make tests both shorter and clearer. When a test says Invoice::factory()->paid()->for($customer)->create(), the intent is obvious at a glance, and the moment your schema changes you fix the data in one factory instead of a hundred tests.

Further Reading

Stay Updated

Subscribe to our newsletter

Get latest news, tutorials, community articles and podcast episodes delivered to your inbox.

Weekly articles
We send a new issue of the newsletter every week on Friday.
No spam
We'll never share your email address and you can opt out at any time.