Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Model Factories for Realistic Test Data

By Kokil Thapa | Last reviewed: September 2026

Tests that pass on empty databases and fail in production usually share one root cause: unrealistic fixtures. Laravel Model Factories for Realistic Test Data solve that by generating records that respect your schema, foreign keys, validation rules, and domain constraints — not just random strings. On production Laravel applications I maintain, factories are the first thing I wire up after migrations because they pay back every time someone writes a feature test, seeds a staging database, or reproduces a bug locally. This guide covers factory design from a bare UserFactory through nested relationships, states, and CI-safe patterns for Laravel 13 on PHP 8.3+.

What are Laravel Model Factories and why do they matter for realistic test data?

A model factory is a PHP class that defines a default attribute blueprint for an Eloquent model. Laravel ships with factory support built in; you generate one with Artisan, then call it from tests, seeders, or Tinker. The goal is not randomness for its own sake — it is plausible data that exercises the same code paths real users trigger.

In my experience working on production Laravel applications — booking portals, eCommerce carts, legal-tech document workflows — factories become the contract between database schema and test suite. When a migration adds a NOT NULL column or a new foreign key, the factory breaks immediately in CI, which is exactly what you want.

Factory Data PipelineFactory Classdefinition()Fakerrealistic valuesEloquentmake / createPHPUnit / Pestfeature testsDatabaseSeederstaging dataTinkerlocal debugMySQL / PostgreSQLRecords that satisfy constraints and relationships
How Laravel Model Factories for Realistic Test Data flow from factory definitions through Faker into tests, seeders, and the database.

Laravel 13 continues the factory API introduced in Laravel 8: a single definition() method, optional configure() for after-creating hooks, and first-class HasFactory on models. You need PHP 8.3 or higher and Composer 2.10 for current project templates. If you are still on Laravel 12, the same patterns apply — only the default directory layout and some testing helpers differ slightly.

When factories beat manual fixtures

  • Schema drift: Adding a column updates one factory file instead of forty test arrays.
  • Relationship depth: An order test needs a user, products, and line items — factories compose that in one line.
  • Staging parity: Seeders reuse the same factories tests rely on, so QA sees data shaped like production edge cases.
  • Deterministic debugging: Combined with fake()->seed(), you reproduce the exact same "random" values across runs.

For broader pipeline context — how factories fit beside migrations, seeders, and CI — see the companion piece on test data management for pipelines.

How do you create a Laravel Model Factory with realistic attributes?

Start from Artisan. Laravel places factories in database/factories and expects a matching model under app/Models.

php artisan make:model Booking -mf
php artisan make:factory BookingFactory --model=Booking

The -mf flag creates the model, migration, and factory together — a pattern I use on every new domain entity in booking systems like those built for trek and tour management platforms.

Baseline factory with domain-aware Faker usage

Random strings fail validation. Realistic factories mirror your Form Request rules:

<?php

namespace Database\Factories;

use App\Models\Booking;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class BookingFactory extends Factory
{
    protected $model = Booking::class;

    public function definition(): array
    {
        $start = fake()->dateTimeBetween('+1 day', '+30 days');
        $end   = (clone $start)->modify('+7 days');

        return [
            'uuid'       => (string) Str::uuid(),
            'user_id'    => User::factory(),
            'reference'  => strtoupper(fake()->bothify('BK-####-??')),
            'status'     => fake()->randomElement(['pending', 'confirmed', 'cancelled']),
            'starts_at'  => $start,
            'ends_at'    => $end,
            'guests'     => fake()->numberBetween(1, 12),
            'total_npr'  => fake()->numberBetween(15000, 450000),
            'notes'      => fake()->optional(0.3)->sentence(),
        ];
    }
}

Notice three realism techniques:

  1. Constrained enumsstatus only uses values your application actually handles.
  2. Derived datesends_at always follows starts_at, preventing impossible ranges that hide bugs.
  3. Lazy foreign keysUser::factory() creates the related user only when needed, keeping tests fast when you override user_id.

Attach the factory to the model with the HasFactory trait:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Booking extends Model
{
    use HasFactory;

    protected $fillable = [
        'uuid', 'user_id', 'reference', 'status',
        'starts_at', 'ends_at', 'guests', 'total_npr', 'notes',
    ];

    protected function casts(): array
    {
        return [
            'starts_at' => 'datetime',
            'ends_at'   => 'datetime',
        ];
    }
}

Locale-specific realism for Nepal-facing apps

For Nepali phone numbers, addresses, or NPR amounts, pass a locale to Faker or hard-code format helpers. A legal-tech portal might store client phone numbers as 10-digit mobile strings starting with 98 or 97 — enforce that in the factory so validation tests mean something:

'phone' => fake()->numerify('98########'),
'pan'   => fake()->optional(0.6)->numerify('#######'),

When you need Bikram Sambat dates in the UI, keep AD timestamps in the database (standard practice) and convert at presentation — your factory should still store valid datetime columns. The Nepali date converter is useful when manually verifying seeded display output, not inside factory definitions.

How do factory states and relationships build realistic datasets?

Default definitions represent the "average" record. States model variants your business logic treats differently — cancelled bookings, admin users, published posts. Without states, tests either mutate records after creation (extra queries, easy to forget) or duplicate factory classes.

Factory States PatternBookingFactorydefault: pendingconfirmed()paid, future datescancelled()refund flag setoverdue()past end dateTest usage: Booking::factory()->confirmed()->create()Composable: ->for($user)->hasItems(3)->confirmed()Each state encodes business rules tests depend on
Factory states turn Laravel Model Factories for Realistic Test Data into composable variants that mirror real booking lifecycles.

Defining states

public function confirmed(): static
{
    return $this->state(fn () => [
        'status'    => 'confirmed',
        'paid_at'   => now(),
        'starts_at' => now()->addDays(14),
        'ends_at'   => now()->addDays(21),
    ]);
}

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

Relationship factories with has(), for(), and afterCreating

Laravel's relationship helpers keep nested data consistent. On a real-world eCommerce system, an order with three line items should reference products that actually exist:

// One order, three items, one known user
$order = Order::factory()
    ->for($user)
    ->has(OrderItem::factory()->count(3), 'items')
    ->create();

// Product with media attachments (Spatie Media Library pattern)
$product = Product::factory()
    ->hasAttached(Category::factory()->count(2))
    ->create();

Use afterCreating when the side effect is not a simple child row — for example, creating a default wallet balance or assigning a role via Spatie Permission:

public function configure(): static
{
    return $this->afterCreating(function (User $user) {
        $user->assignRole('customer');
    });
}

Authorization tests then call User::factory()->admin()->create() with a dedicated admin state that assigns the correct role — a pattern covered in depth alongside Laravel policies and gates.

Sequence and count modifiers

When you need varied but structured data — ten users with unique emails, or incrementing SKU codes — use sequences:

Product::factory()
    ->count(10)
    ->sequence(
        ['sku' => 'SKU-001', 'price_npr' => 500],
        ['sku' => 'SKU-002', 'price_npr' => 1500],
    )
    ->create();

This beats looping in seeders and keeps the variation visible in one place.

What is the difference between model factories, seeders, and manual fixtures?

Teams conflate these three constantly. Each has a job; mixing them creates slow tests and unrealistic staging databases.

ApproachBest forRuns inRealism leverDownside
Model factoriesTests, local debugging, composable seed dataPHPUnit, Pest, Tinker, seedersStates, Faker, relationshipsNeeds maintenance when schema changes
SeedersStaging/demo baseline, role setup, reference datadb:seed, deploy hooksCalls factories with fixed countsNot ideal inside isolated unit tests
Manual fixtures (JSON/YAML arrays)Frozen contract tests, API snapshot testsSpecific test classesExact reproducibilityBrittle; drifts from schema silently
Database factories + RefreshDatabaseFeature tests needing real SQLCI on MySQL/PostgreSQLFull stack fidelitySlower than in-memory SQLite

Symfony projects often reach for Doctrine FixturesBundle instead — the conceptual overlap is similar, as described in the Symfony DoctrineFixturesBundle guide. In Laravel, keep seeders thin: they orchestrate, factories generate.

<?php

namespace Database\Seeders;

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

class DemoSeeder extends Seeder
{
    public function run(): void
    {
        $admin = User::factory()->admin()->create([
            'email' => 'admin@example.test',
        ]);

        Booking::factory()
            ->count(25)
            ->confirmed()
            ->create();

        Booking::factory()
            ->count(5)
            ->cancelled()
            ->create();
    }
}
Test Data Strategy ChoiceFactoriesDynamic, composableSchema-awareBest for testsSeedersOrchestration layerFixed scenariosBest for stagingStatic JSONFrozen snapshotsNo DB neededDrifts quicklyRecommended Laravel stackFactories generate → Seeders compose → Tests call factories directlyReserve JSON fixtures for API contract tests onlyUse RefreshDatabase trait in feature tests
Choosing between factories, seeders, and static fixtures when building Laravel Model Factories for Realistic Test Data workflows.

How do you integrate Laravel Model Factories into PHPUnit and Pest tests?

Feature tests should hit real SQL when the risk warrants it. Laravel's RefreshDatabase trait migrates fresh schema per test class (or uses transactions on supported drivers), then factories populate rows on demand.

PHPUnit example

<?php

namespace Tests\Feature;

use App\Models\Booking;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BookingCancellationTest extends TestCase
{
    use RefreshDatabase;

    public function test_confirmed_booking_can_be_cancelled(): void
    {
        $user    = User::factory()->create();
        $booking = Booking::factory()
            ->for($user)
            ->confirmed()
            ->create();

        $response = $this->actingAs($user)
            ->post("/bookings/{$booking->uuid}/cancel");

        $response->assertRedirect();
        $this->assertDatabaseHas('bookings', [
            'id'     => $booking->id,
            'status' => 'cancelled',
        ]);
    }
}

Pest equivalent

use App\Models\Booking;
use App\Models\User;
use function Pest\Laravel\actingAs;
use function Pest\Laravel\post;

it('cancels a confirmed booking', function () {
    $user    = User::factory()->create();
    $booking = Booking::factory()->for($user)->confirmed()->create();

    actingAs($user)
        ->post("/bookings/{$booking->uuid}/cancel")
        ->assertRedirect();

    expect($booking->fresh()->status)->toBe('cancelled');
});

Prefer make() over create() when the database never needs the row — policy unit tests that only inspect model state, for example. That keeps suites fast on large codebases.

Database driver choices

SQLite in memory is fine for simple CRUD. When you rely on MySQL-specific JSON columns, full-text indexes, or PostgreSQL constraints, run CI against the same engine you use in production. I've seen factories pass on SQLite and fail on MySQL 9.7 because of JSON validation differences — the fix is aligning the test database, not weakening the factory. See PostgreSQL for Laravel developers if that is your production engine.

Deterministic Faker seeds

Flaky tests often trace back to random data crossing a validation threshold. Seed Faker in TestCase::setUp() or a Pest before hook:

protected function setUp(): void
{
    parent::setUp();
    fake()->seed(12345);
}

Now fake()->email() returns the same address every run, while still looking realistic. Change the seed when you need a different but still reproducible dataset.

For API endpoints, pair factories with Laravel API best practices — factory-created models should flow through the same transformers and resource classes production uses, not bypass them in tests.

What factory anti-patterns break realistic test data in production Laravel apps?

Factories age badly when treated as throwaway scaffolding. These mistakes show up repeatedly on client projects during upgrades.

1. Defaults that violate business rules

A factory that sets ends_at before starts_at might still insert if the database lacks a check constraint — then your cancellation job never fires in tests. Encode invariants in the factory the same way you would in a Form Request.

2. Hard-coded IDs

'category_id' => 1 breaks the moment seed order changes. Use Category::factory() or Category::factory()->create(['slug' => 'flowers']) when the test cares about a specific category.

3. God factories with twelve states

Split into dedicated factory classes or use traits when a single file exceeds roughly 150 lines. OrderFactory and SubscriptionOrderFactory can share a Concerns\HasNepaliPricing trait without merging unrelated states.

4. Calling external APIs inside factories

Never hit Stripe, SMS gateways, or Khalti from afterCreating. Fake the gateway in tests and let the factory only set local columns like payment_reference. Payment integration tests belong in dedicated cases with mocked HTTP, as outlined in Laravel payment integrations.

5. Skipping factories for "simple" models

Lookup tables (countries, statuses) still deserve factories or seeder constants. Tests that assume ID 1 exists fail mysteriously after RefreshDatabase on a empty schema.

Factory Anti-Patterns to AvoidHard-coded foreign keysUse Model::factory() insteadInvalid date rangesDerive end from startExternal HTTP in hooksMock gateways in testsRandom enum valuesMatch app constants onlyFix: mirror Form Request rulesFactories should fail when schema or validation changesTreat factory maintenance as part of every migration PR
Anti-patterns that undermine Laravel Model Factories for Realistic Test Data and how to correct them in production codebases.

On the Nepal Gift Card platform and similar Laravel eCommerce builds, factories for orders, gift codes, and redemption logs mirror the exact status transitions the fulfilment service expects — so a green CI run actually means something when Dashain-season traffic spikes.

Official references worth bookmarking: the Laravel 13 Eloquent factories documentation and the database testing guide. Faker's formatter list lives in the FakerPHP documentation.

When teams explore AI-assisted test generation in CI, factories remain the ground truth — generated tests still need realistic starting records, as discussed in AI for test generation in CI. Professional testing and optimization engagements often start with a factory audit because it is the fastest way to raise coverage quality without writing throwaway tests.

Key Takeaways

  • Define factory defaults that satisfy the same rules as your Form Requests — invalid random data hides bugs rather than finding them.
  • Use states for business variants (confirmed, cancelled, admin) and compose them with for(), has(), and count() instead of post-create mutations.
  • Keep seeders as orchestrators that call factories; do not duplicate attribute arrays in seeder files.
  • Prefer make() when persistence is unnecessary, create() for feature tests, and seed Faker when flakiness appears.
  • Run CI against the same database engine as production when factories rely on JSON, constraints, or dialect-specific types.
  • Update factories in the same pull request as every migration — treat them as part of the schema contract, not optional test glue.

People Also Ask

What is the difference between make() and create() in Laravel factories?

make() builds an in-memory model instance without inserting a database row. create() persists the record. Use make() for unit tests that inspect computed attributes or policy methods that do not query the database. Use create() when foreign keys, unique indexes, or feature tests require real rows.

Can Laravel factories work without Faker?

Yes. Return static or computed values directly from definition(). Faker is the default convenience layer, but you can use enums, now(), or custom generators — especially for regulated fields like PAN numbers or fixed-price catalogue items.

How many records should seeders create with factories?

Staging needs enough volume to expose slow queries — often hundreds or low thousands per core table. Local developer seeds can stay smaller (ten to fifty) for speed. Never seed production with random factory data; production data comes from real usage or controlled imports.

Do factories slow down Laravel test suites?

Unnecessary create() calls and deep relationship trees are the usual bottleneck, not Faker itself. Use make(), override foreign keys with existing records, and share setup in beforeEach when a test group needs the same parent models. Database transactions via RefreshDatabase also help on MySQL and PostgreSQL.

Build test data that matches how your Laravel app actually runs

Laravel Model Factories for Realistic Test Data are not a testing nicety — they are how you keep migrations, validation, authorization, and business logic aligned as the application grows. Start with one well-crafted factory per aggregate root, add states for the variants your controllers branch on, and wire seeders to reuse the same definitions your CI suite depends on. That is the baseline I apply on every new custom Laravel project, from legal-tech portals to multi-vendor marketplaces documented in the portfolio.

If your test suite still relies on hand-typed arrays or seed data that does not reflect production rules, a focused factory refactor typically pays back within the first sprint. For hands-on help designing factories, seeders, and CI test strategy on Laravel 13, get in touch via the contact page — or explore modern Laravel architecture practices and building RESTful APIs with Laravel for related foundations. You can also validate JSON API payloads during factory-driven tests with the free JSON formatter on this site.

Frequently Asked Questions

A model factory is a PHP class in database/factories that defines a default attribute blueprint for an Eloquent model. It uses Faker to generate plausible records that respect your schema, foreign keys, and validation rules — not random strings that pass empty databases but fail against real business logic.

Run php artisan make:model Booking -mf or php artisan make:factory BookingFactory --model=Booking. Define a definition() method with domain-aware Faker values: constrained enums for status, derived dates so ends_at follows starts_at, and lazy foreign keys like User::factory() instead of hard-coded IDs. Attach HasFactory to the matching model under app/Models. Mirror your Form Request validation rules so factory data exercises the same code paths production users trigger.

Laravel 13 requires PHP 8.3 or higher and Composer 2.10. Laravel 12 uses the same factory API with only minor layout differences.

Factories generate composable, Faker-driven records ideal for PHPUnit, Pest, Tinker, and seeders — they break immediately when schema drifts, which is desirable. Seeders orchestrate baseline staging or demo data by calling factories with fixed counts and known emails like admin@example.test. Manual JSON or YAML fixtures suit frozen contract or API snapshot tests but drift silently from migrations. In Laravel, keep seeders thin: they orchestrate, factories generate.

States model business variants your logic treats differently — confirmed bookings with paid_at set, cancelled bookings with cancelled_at, or admin users with assigned roles. Define them as methods returning $this->state(fn () => [...]). Without states, tests mutate records after creation with extra queries or duplicate factory classes. Compose states in one line: Booking::factory()->for($user)->confirmed()->create(). This mirrors real booking lifecycles and keeps authorization tests readable.

Use for($user) to attach a known parent, has(OrderItem::factory()->count(3), 'items') to create nested children, and hasAttached(Category::factory()->count(2)) for many-to-many links. Use configure() with afterCreating for side effects that are not simple child rows — assigning a Spatie Permission role or creating a default wallet balance. Never call external APIs like Stripe or Khalti from afterCreating; set local columns only and mock gateways in dedicated payment tests.

Use make() when the database never needs the row — policy unit tests inspecting model state only. Use create() when tests hit real SQL via RefreshDatabase.

Add RefreshDatabase to your test class — it migrates a fresh schema per class or uses transactions on supported drivers. Create data on demand: $booking = Booking::factory()->for($user)->confirmed()->create(). Assert with assertDatabaseHas or Pest expect()->toBe(). Pair factory-created models with the same API transformers and resource classes production uses. For endpoints, follow Laravel API best practices so tests reflect real response shapes, not bypassed serialization.

Random data crossing validation thresholds causes intermittent failures. Seed Faker in TestCase::setUp() or a Pest before hook: fake()->seed(12345). The same fake()->email() and other values return every run while still looking realistic. Change the seed when you need a different but reproducible dataset. Combined with factory states, you can reproduce exact bug conditions locally and in CI without brittle hard-coded arrays.

SQLite in memory is fine for simple CRUD. When production uses MySQL-specific JSON columns, full-text indexes, or PostgreSQL constraints, run CI against the same engine — I've seen factories pass on SQLite and fail on MySQL 9.7 due to JSON validation differences. Align the test database with production rather than weakening factory definitions. See PostgreSQL for Laravel developers if that is your production engine.

Keep seeders thin orchestrators. A DemoSeeder might create one admin via User::factory()->admin()->create(['email' => 'admin@example.test']), then Booking::factory()->count(25)->confirmed()->create() plus five cancelled bookings. QA sees data shaped like production edge cases because seeders call the same factories tests rely on. This gives staging parity without maintaining separate fixture files that drift from schema changes.

Common mistakes: defaults violating business rules like ends_at before starts_at; hard-coded category_id => 1 that breaks when seed order changes; god factories exceeding roughly 150 lines instead of splitting into OrderFactory and traits; calling Stripe or SMS gateways from afterCreating; skipping factories for lookup tables so tests assume ID 1 exists after RefreshDatabase on an empty schema. Encode invariants in factories the same way you would in Form Requests.

Use lazy relationship factories: 'user_id' => User::factory() creates the related user only when needed, keeping tests fast when you override user_id. When a test needs a specific parent, pass for($user) or create the related model first with a meaningful attribute like Category::factory()->create(['slug' => 'flowers']). Hard-coded IDs break the moment migration or seed order changes and hide relationship bugs your feature tests should catch.

Pass a Faker locale or hard-code format helpers matching local validation: phone as fake()->numerify('98########') for 10-digit mobiles starting with 98 or 97, PAN as fake()->optional(0.6)->numerify('#######'), and total_npr with fake()->numberBetween(15000, 450000). Store AD timestamps in datetime columns even when the UI shows Bikram Sambat — convert at presentation, not inside factory definitions. This ensures validation tests for legal-tech portals and eCommerce carts mean something in production.

Nothing beyond existing Laravel tooling — factories ship built in with Laravel 8 and later. The investment is developer time wiring factories after migrations, typically a few hours per domain entity on booking or eCommerce projects.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: