laravel test factories

Building Robust Laravel Test Factories for Reliable Automated Testing

Modern Laravel applications run on automated testing. Not as an afterthought — as a structural requirement. And yet, most teams spend their energy writing tests, not designing the factories those tests depend on. That’s a mistake that compounds over time.

Weak factories produce brittle tests. Schema changes break them. Fake data collides. Hidden object graphs make your CI pipeline slow enough that developers start skipping it. By the time you feel the pain, the damage is already architectural. The production-grade AI architecture guide covers the contract and governance layer that these factories need to test reliably.

This guide covers how to design Laravel test factories that actually hold up: structured states, relationship modeling, sequence-driven determinism, and performance patterns for suites running thousands of tests. We’re targeting Laravel 11 and 12 throughout. If you’re still on Laravel 10, upgrade — seriously.

Why Factory Design Is an Architectural Decision

Most developers treat factories as boilerplate. They’re not. A factory is your domain model in miniature — it defines what “valid data” looks like across your entire test suite. If it’s wrong, every test downstream inherits that wrongness silently.

Weak factories typically cause:

  • Tests that fail intermittently due to Faker randomness
  • Unrealistic data that never surfaces edge cases your app actually hits in production
  • Repeated create() setup scattered across test files
  • Slow suites caused by unnecessary database writes and deep relationship graphs being created when nobody asked for them

Strong factories give you consistent domain modeling, reusable test scenarios, and a codebase where a new developer can read a test and immediately understand the system state it’s asserting against. For the output-side of that contract — validating AI output in Laravel before it reaches your application layer — see the schema validation guide.

Laravel’s factory system — backed by the Service Container and Eloquent — is powerful enough to model complex domains. Most teams just don’t push it far enough.

Prerequisites

You’ll need:

  • PHP 8.2+
  • Laravel 11 or 12
  • A configured testing database (SQLite in-memory works well for most suites)
  • Pest (preferred) or PHPUnit

Pest ships as the default test runner in new Laravel 11/12 projects. If you haven’t made the switch, you should — and if you want a broader view of where it sits in a modern workflow, our 2026 Laravel tooling breakdown covers the full stack.

For official factory documentation, see the Laravel Eloquent Factories docs.

Creating a Basic Factory

Laravel factories live in database/factories. Generate one:

php artisan make:factory PostFactory --model=Post
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
    public function definition(): array
    {
        return [
            'title'     => fake()->unique()->sentence(),
            'slug'      => str()->slug(fake()->unique()->words(3, true)),
            'content'   => fake()->paragraphs(3, true),
            'published' => false,
        ];
    }
}

Two things to note immediately. First, fake()->unique()->sentence() — not fake()->sentence(). Uniqueness constraints on slugs and titles are real, and non-unique fakers cause flaky tests that are miserable to debug. Second, str()->slug() over fake()->slug() — Faker’s slug helper respects locale, which means you can get non-ASCII characters depending on your environment. Don’t trust it for slug fields.

Factory States: Your Domain Vocabulary

This is where most factory implementations stop being useful. States are not optional convenience methods — they are your primary tool for communicating domain intent inside a test.

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

public function scheduled(): static
{
    return $this->state(fn (array $attributes) => [
        'published'    => false,
        'scheduled_at' => now()->addDay(),
    ]);
}

public function draft(): static
{
    return $this->state(fn (array $attributes) => [
        'published'    => false,
        'scheduled_at' => null,
    ]);
}

Note the return type: : static, not : Factory. This matters for IDE type inference and for chaining states correctly when you’re building complex scenarios. If you’re still typing : Factory here, fix it now.

Usage:

Post::factory()->published()->create();
Post::factory()->scheduled()->create();

A test like this is self-documenting:

public function test_published_posts_appear_in_feed(): void
{
    Post::factory()->published()->count(3)->create();
    Post::factory()->draft()->count(2)->create();

    $this->get('/feed')
        ->assertJsonCount(3, 'data');
}

No comments required. The factory states are the documentation.

Senior Dev Tip: Never encode business logic inside a state. A state represents a data condition, not an application process. If you find yourself calling a service class or dispatching an event inside a state method, stop — you’re blurring the boundary between test setup and test subject. That always ends in circular dependencies and tests that fail for the wrong reasons.

Modeling Relationships

Factories should replicate the relationship structure your application actually depends on.

// PostFactory
public function definition(): array
{
    return [
        'user_id' => User::factory(),
        'title'   => fake()->unique()->sentence(),
        'content' => fake()->paragraph(),
    ];
}

Laravel resolves the User::factory() lazily — it only fires if you don’t supply a user_id explicitly. That’s clean. Use it.

For explicit control:

Post::factory()
    ->for(User::factory()->state(['role' => 'author']))
    ->create();

For has-many relationships, two patterns exist. The magic has* method:

Post::factory()->hasComments(5)->create();

This requires the comments() relationship to be defined on the Post model — a gotcha that trips up junior devs constantly. If you’re getting “Call to undefined method” errors here, that’s why.

The explicit afterCreating approach gives you more control:

public function configure(): static
{
    return $this->afterCreating(function (Post $post) {
        Comment::factory()->count(5)->for($post)->create();
    });
}

Use afterCreating when the related model needs to know about the parent at creation time, or when you need conditional relationship creation based on state.

Sequences for Deterministic Variation

When you’re testing filtering, pagination, or sorting, you need predictable variation — not random variation. Sequences deliver that.

Post::factory()
    ->count(4)
    ->sequence(
        ['published' => true,  'published_at' => now()->subDays(3)],
        ['published' => false, 'published_at' => null],
        ['published' => true,  'published_at' => now()->subDay()],
        ['published' => false, 'published_at' => null],
    )
    ->create();

This is significantly more useful than using ->count(4) with a random published state. Your ordering and filtering tests need to know exactly what data exists. Randomness is your enemy here.

Performance Patterns for Large Test Suites

Once your suite crosses a few hundred tests, factory performance starts mattering. There are three levers.

Use make() instead of create() when you don’t need persistence:

$post = Post::factory()->make();
// No database write. Instant.

Any test that only inspects model attributes or calls methods that don’t touch the database should be using make(). This is the lowest-effort, highest-return optimisation available.

Use LazilyRefreshDatabase over RefreshDatabase for large suites:

use Illuminate\Foundation\Testing\LazilyRefreshDatabase;

class PostTest extends TestCase
{
    use LazilyRefreshDatabase;
}

RefreshDatabase runs migrations before every test class. LazilyRefreshDatabase defers migration until the first database operation actually happens. For suites with many unit tests mixed in, the difference is meaningful.

Disable model events when seeding test data at volume:

Post::withoutEvents(fn () => Post::factory()->count(500)->create());

Observers and listeners running on 500 factory records will murder your suite runtime. This is especially relevant if you’re using activity logging or audit trail packages.

If your project has grown to the point where test organisation and generator scaffolding feel like overhead, Laravel Boost addresses exactly that gap at the workflow level.

Common Anti-Patterns (and What They Cost You)

1. Business logic inside factories

// Don't do this
'status' => $this->computePublishingStatus($this->attributes),

The test should control state explicitly. Factories that compute state are factories that hide bugs.

2. Deep object graphs as defaults

If your UserFactory automatically creates a Profile, a Subscription, and a BillingAddress by default, every test that touches a user pays that cost — even if it only cares about the user’s email. Keep defaults minimal. Build depth explicitly.

3. Non-unique faker fields on columns with unique constraints

// This will fail intermittently on large suites
'email' => fake()->email(),
// This won't
'email' => fake()->unique()->safeEmail(),

Intermittent uniqueness failures are the worst kind of test failure. They pass on the developer’s machine, fail in CI, and take 20 minutes to reproduce. ->unique() is mandatory on any field with a database-level unique constraint.

Gotcha: fake()->unique() resets between test runs, but not between factory calls within the same test. If you call UserFactory 200 times in a single test, Faker’s unique generator will eventually exhaust its pool and throw a \OverflowException. For high-volume seeding, use fake()->unique(true) to reset the pool, or generate uniqueness from a sequence instead.

A Real-World Integration Test

This is what a well-designed factory workflow looks like end-to-end:

public function test_author_can_publish_their_own_draft(): void
{
    $author = User::factory()->create();

    $post = Post::factory()
        ->for($author)
        ->draft()
        ->create();

    $this->actingAs($author)
        ->post("/posts/{$post->id}/publish")
        ->assertRedirect();

    expect($post->fresh())
        ->published->toBeTrue()
        ->published_at->not->toBeNull();
}

Minimal setup. Explicit state. Zero ambiguity about what’s being tested. This is the target.

Summary

Well-designed factories are not a nicety — they are load-bearing infrastructure for your test suite. The patterns that matter:

  • Use static return types on state methods
  • Keep defaults minimal; model depth explicitly
  • Apply ->unique() to any field with a uniqueness constraint
  • Use sequences for deterministic variation in sort/filter tests
  • Prefer make() over create() wherever persistence isn’t needed
  • Use LazilyRefreshDatabase in large suites
  • Never embed business logic in a factory state

The cost of getting this wrong is a test suite that slowly becomes a tax on your team instead of an asset. Invest in the factory layer early, and it will hold up as your domain grows.

For the full official reference, see the Laravel Eloquent Factories documentation.

Subscribe
Notify of
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Navigation
Scroll to Top