Factories and seeders: a practical guide for developers
Getting realistic test and demo data into your Laravel application shouldn't require manual effort or brittle fixtures. When you're building features across e-commerce platforms, SaaS applications, or ride-hailing systems, you need factories to generate fake model data quickly and states to handle variations like suspended users or archived orders. This article covers factories and seeders: a practical guide for developers working in production environments where data consistency matters.
Why factories matter more than you think
Factories define fake model data in a way that's repeatable, maintainable, and realistic. Instead of hardcoding test data or creating JSON fixtures that drift out of sync with your schema, you define how a model should be created once and reuse that definition everywhere.
Consider a travel booking system where you need to test refund logic. Without a factory, you might write:
$booking = new Booking();
$booking->user_id = 1;
$booking->destination = 'Bangkok';
$booking->departure_date = '2024-02-15';
$booking->return_date = '2024-02-22';
$booking->total_price = 1500.00;
$booking->status = 'confirmed';
$booking->save();Now imagine testing five different scenarios. You're duplicating this code and changing fields manually. A factory eliminates that:
php artisan make:factory BookingFactoryclass BookingFactory extends Factory
{
protected $model = Booking::class;
public function definition(): array
{
return [
'user_id' => User::factory(),
'destination' => $this->faker->city(),
'departure_date' => $this->faker->dateTimeBetween('+1 day', '+30 days'),
'return_date' => $this->faker->dateTimeBetween('+31 days', '+60 days'),
'total_price' => $this->faker->randomFloat(2, 500, 5000),
'status' => 'confirmed',
];
}
}Now your tests are cleaner: Booking::factory()->create(). The factory handles relationships, realistic values via Faker, and consistency. When your schema changes, you update one place. When your CRM system needs test accounts with phone numbers, email domains, and address formats that match real data, factories generate them correctly without manual intervention.
Defining model factories with realistic data
A factory's definition() method returns an array of attributes. Each time you call create() or make(), it generates a fresh set of values. Use Faker to generate realistic data: email addresses, phone numbers, dates, text, UUIDs, and more.
For an e-commerce product factory:
class ProductFactory extends Factory
{
protected $model = Product::class;
public function definition(): array
{
return [
'sku' => $this->faker->unique()->bothify('PROD-###-???'),
'name' => $this->faker->words(3, true),
'description' => $this->faker->paragraphs(2, true),
'price' => $this->faker->randomFloat(2, 10, 1000),
'cost' => $this->faker->randomFloat(2, 5, 500),
'stock' => $this->faker->numberBetween(0, 500),
'category_id' => Category::factory(),
'created_at' => $this->faker->dateTimeBetween('-1 year'),
];
}
}Notice several things here: unique() ensures SKUs don't collide, Faker methods like bothify() create realistic format strings, relationships are resolved via nested factories, and timestamps reflect real scenarios. When your payment processing team needs to test refunds on products from the last year, they get authentic data without gaming the system.
States for handling real-world variations
Not every user is active, not every order is shipped, and not every subscription is current. States let you create factory variations without defining separate factories.
For a CRM user model that tracks account status:
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'phone' => $this->faker->phoneNumber(),
'status' => 'active',
'email_verified_at' => now(),
'password' => Hash::make('password'),
];
}
public function suspended(): static
{
return $this->state(function (array $attributes) {
return [
'status' => 'suspended',
'suspended_at' => now()->subDays($this->faker->numberBetween(1, 90)),
];
});
}
public function unverified(): static
{
return $this->state(function (array $attributes) {
return [
'email_verified_at' => null,
'status' => 'pending',
];
});
}
public function admin(): static
{
return $this->state(function (array $attributes) {
return [
'role' => 'admin',
'verified_at' => now(),
];
});
}
}Usage is intuitive:
// In tests
$activeUser = User::factory()->create();
$suspendedUser = User::factory()->suspended()->create();
$adminUser = User::factory()->admin()->create();
$unverifiedUsers = User::factory()->unverified()->count(5)->create();States compose naturally. If you need a suspended admin, you chain: User::factory()->admin()->suspended()->create(). In ride-hailing systems, you might have driver states for offline, on-trip, or blocked. In SaaS, you track trial, active, or churned accounts. States keep your factories DRY and your test setup readable.
Seeders for initial and demo data
Seeders populate your database with initial data for development, staging, or demos. Use factories inside seeders to keep the logic consistent.
php artisan make:seeder DatabaseSeederclass DatabaseSeeder extends Seeder
{
public function run(): void
{
// Create core reference data
Category::create(['name' => 'Electronics', 'slug' => 'electronics']);
Category::create(['name' => 'Clothing', 'slug' => 'clothing']);
// Use factories for scale
User::factory()->count(50)->create();
User::factory()
->count(10)
->admin()
->create();
// Create products with relationships
Product::factory()
->count(100)
->create()
->each(function ($product) {
// Attach reviews for each product
Review::factory()
->count($this->faker->numberBetween(0, 10))
->for($product)
->create();
});
// Create realistic orders from existing users
User::factory()
->has(Order::factory()->count(3))
->count(30)
->create();
}
}Run seeders with php artisan db:seed or refresh your database and seed in one command: php artisan migrate:fresh --seed. This gives you a realistic demo environment in seconds. Your frontend team can demo features with 100 products and 50 users without waiting for manual data entry. Your QA team can reset to a known state between test runs.
Using factories extensively in tests
Factories shine in test suites. They reduce noise, improve readability, and make tests maintainable as your schema evolves.
class RefundProcessingTest extends TestCase
{
public function test_refund_reduces_inventory()
{
// Arrange
$product = Product::factory()->create(['stock' => 10]);
$order = Order::factory()
->for($product)
->create(['status' => 'shipped']);
// Act
(new RefundService())->process($order);
// Assert
$this->assertEquals(11, $product->fresh()->stock);
}
public function test_cannot_refund_cancelled_order()
{
$order = Order::factory()
->cancelled()
->create();
$this->expectException(CannotRefundCancelledOrder::class);
(new RefundService())->process($order);
}
public function test_suspended_users_cannot_request_refunds()
{
$user = User::factory()->suspended()->create();
$order = Order::factory()
->for($user)
->create(['status' => 'shipped']);
$response = $this->actingAs($user)
->postJson('/api/orders/' . $order->id . '/refund');
$response->assertStatus(403);
}
}Each test is self-contained and readable. You create exactly the data you need, nothing more. When the Order model gains a payment_method_id column, you update the factory once and all tests adapt automatically. In WebSocket-based systems or Kafka consumers, factories let you simulate different event states without manual database manipulation.
Common mistakes
- Over-specifying in factories. Don't hardcode values that should vary. Use Faker to generate realistic, random data for each instance. If all test products are named "Test Product", you miss bugs that occur with longer or special-character names.
- Forgetting to resolve relationships. Use nested factories for foreign keys:
'user_id' => User::factory(). Don't hardcode IDs; it breaks when tests run in parallel or in different orders. - Creating too much data in seeders. Seeders should be fast. If your
db:seedtakes 30 seconds, developers won't run it often. Keep seeder counts reasonable or make them configurable via environment variables. - Not using states for status variations. Repeating the same state logic across multiple factories creates maintenance debt. Define a state once, reuse everywhere.
- Mixing test data with production-like data. Don't seed real customer emails or phone numbers in development. Faker gives you realistic formats without exposing sensitive information.
- Ignoring factory performance in large test suites. If you're creating 1,000 users in a test, consider using
make()instead ofcreate()if you don't need them persisted. For tests that do persist many records, use raw queries or batch inserts to speed up setup.
Key takeaways
- Factories define fake model data in a reusable, maintainable way. They replace manual test fixtures and adapt automatically when your schema changes.
- States handle real-world variations like suspended users or shipped orders. Chain states naturally without creating separate factories.
- Seeders provide initial and demo data for development and staging. Build them from factories to keep data generation logic centralized.
- Use factories heavily in tests to reduce noise, improve readability, and make assertions clear. Each test should create only the data it needs.
- Leverage Faker for realistic values: emails, phone numbers, dates, text, and format strings. Avoid hardcoded test data that misses edge cases.
- Run
php artisan migrate:fresh --seedto reset your development environment to a known state in seconds.






Comments
Loading comments…