
September 07, 2026
15 min read
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+.
database/factories, call Model::factory()->create(), and compose states and relationships so data matches real business rules.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.
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:
- Constrained enums —
statusonly uses values your application actually handles. - Derived dates —
ends_atalways followsstarts_at, preventing impossible ranges that hide bugs. - Lazy foreign keys —
User::factory()creates the related user only when needed, keeping tests fast when you overrideuser_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.
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.
| Approach | Best for | Runs in | Realism lever | Downside |
|---|---|---|---|---|
| Model factories | Tests, local debugging, composable seed data | PHPUnit, Pest, Tinker, seeders | States, Faker, relationships | Needs maintenance when schema changes |
| Seeders | Staging/demo baseline, role setup, reference data | db:seed, deploy hooks | Calls factories with fixed counts | Not ideal inside isolated unit tests |
| Manual fixtures (JSON/YAML arrays) | Frozen contract tests, API snapshot tests | Specific test classes | Exact reproducibility | Brittle; drifts from schema silently |
Database factories + RefreshDatabase | Feature tests needing real SQL | CI on MySQL/PostgreSQL | Full stack fidelity | Slower 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();
}
} 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.
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 withfor(),has(), andcount()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
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.

