Learn to master Laravel database factories and seeders to generate realistic test data. Create state modifiers and automate your testing workflow efficiently.
Previously in this course, we explored Testing Events and Jobs in Laravel: A Guide to Reliable Systems to ensure our background processes behave as expected. While testing async tasks is vital, those tests often fail if the underlying database lacks the necessary structure or relational data. In this lesson, we move beyond manual inserts and hardcoded IDs by mastering database factories and seeders to build a robust, reproducible testing environment for our project board.
At the intermediate level, you shouldn't be manually creating users or tasks in every test file. Doing so leads to "brittle" tests—where changing a column name in your migration forces you to rewrite dozens of test cases.
Laravel's factories act as blueprints for your models. They use the faker library to generate realistic data (names, emails, timestamps) automatically. Seeders, on the other hand, are the orchestrators; they use these factories to populate your database with initial state, whether for development, staging, or integration testing.
Let’s advance our project board by creating a factory for our Task model. Run php artisan make:factory TaskFactory.
In your database/factories/TaskFactory.php, you define the default state. This ensures that every time you call Task::factory(), you get a valid, persisted database record.
PHPpublic function definition(): array { return [ 'title' => fake()->sentence(), 'description' => fake()->paragraph(), 'status' => 'pending', 'user_id' => \App\Models\User::factory(), #6A9955">// Automatically creates a user 'due_at' => fake()->dateTimeBetween('now', '+1 month'), ]; }
Notice how user_id calls the User::factory(). Laravel is smart enough to handle these nested relationships automatically, ensuring that when you create a task, a parent user is created alongside it.
One of the most powerful features of factories is the ability to define "states." Instead of manually overriding attributes in every test, define a named state in your factory:
PHPpublic function overdue(): static { return $this->state(fn(array $attributes) => [ 'due_at' => now()->subDay(), 'status' => 'pending', ]); }
Now, in your feature tests, you can write expressive, readable code:
PHP#6A9955">// In your test file $task = Task::factory()->overdue()->create(); $this->getJson('/api/tasks') ->assertJsonFragment(['status' => 'pending']);
While factories are for individual records, seeders are for populating the application state. Use them to set up admin users, default categories, or initial project boards.
Open database/seeders/DatabaseSeeder.php. You should use seeders to call other specific seeders to keep your logic clean:
PHPpublic function run(): void { #6A9955">// Create 10 users, each with 3 tasks \App\Models\User::factory(10) ->has(Task::factory()->count(3)) ->create(); }
Your task is to prepare the database for our project board.
ProjectSeeder that generates 5 projects.has() method on your User factory to attach tasks to these users.php artisan db:seed.This ensures that whenever you pull down the code, you have a fully functional environment ready for development.
create(): Use make() when you only need an object instance and don't need to hit the database. It is significantly faster for unit tests.user_id => 1 in tests. Always use factories (user_id => User::factory()) so the test remains decoupled from specific database IDs.fake() for everything. If you hardcode 'Task Title', you might miss edge cases where a user enters a 255-character string or special characters that break your UI.DatabaseSeeder clean. If you have complex setup logic, move it into specific classes like ProjectSeeder or DemoDataSeeder.We’ve covered the fundamentals of generating consistent, realistic data. By leveraging factories and states, we've moved away from fragile, hardcoded tests toward a system that evolves with our schema. Seeders provide the final piece of the puzzle, ensuring our development environment is always ready for action.
Up next: We will explore API Versioning Strategies to ensure our project board can evolve without breaking existing client integrations.
Master non-breaking migrations and safe rollback procedures. Learn the expand-and-contract pattern to evolve your database schema without production downtime.
Read moreLearn how to implement database query caching in Laravel to reduce server load. Master cache eviction strategies to ensure data integrity in high-traffic apps.
Database Factories and Seeding