Better Test Data with Laravel Model Factories and States
Model factories are the fastest way to build test data in Laravel, but most people stop at the basics. States, relationships, and sequences turn factories into a tiny domain language for describing exactly the scenario a test needs. Here are the pieces worth mastering.
Start With a Good Base Definition
The factory's definition() should return a valid, realistic record. Keep it minimal but complete, so every generated model is usable without extra setup.
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
class InvoiceFactory extends Factory
{
public function definition(): array
{
return [
'number' => 'INV-' . $this->faker->unique()->numberBetween(1000, 9999),
'amount_cents' => $this->faker->numberBetween(500, 500000),
'status' => 'pending',
];
}
}
States Describe Variations
A state is a named tweak to the base definition. Instead of overriding attributes inline all over your tests, give the meaningful variations names.
public function paid(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'paid',
'paid_at' => now(),
]);
}
public function void(): static
{
return $this->state(fn () => ['status' => 'void']);
}
Now your tests read like sentences:
$paid = Invoice::factory()->paid()->create();
$overdue = Invoice::factory()->count(3)->void()->create();
Relationships in One Line
Factories compose. You can attach related models directly, and Laravel wires up the foreign keys for you.
// A customer with 5 paid invoices
$customer = Customer::factory()
->has(Invoice::factory()->count(5)->paid())
->create();
// From the other direction, using the magic for* method
$invoice = Invoice::factory()
->for(Customer::factory()->state(['name' => 'PHP Architect']))
->create();
Sequences for Varying Data
When you need a set of records that differ in a controlled way, a sequence cycles through values as models are created.
use Illuminate\Database\Eloquent\Factories\Sequence;
$invoices = Invoice::factory()
->count(6)
->state(new Sequence(
['status' => 'paid'],
['status' => 'pending'],
))
->create();
That produces three paid and three pending invoices, alternating. Sequences are ideal for testing filters and reports where you need a known mix.
Run Logic After Creation
The afterCreating hook lets a factory perform side effects once the model exists, which is handy for things that cannot be set as plain attributes.
public function configure(): static
{
return $this->afterCreating(function (Invoice $invoice) {
$invoice->recalculateTotals();
});
}
A Quick Tip: make() vs create()
Use create() when the test needs the record in the database, and make() when you only need an in-memory instance. make() skips the database entirely, which keeps unit tests that do not touch persistence fast.
Well-designed factories make tests both shorter and clearer. When a test says Invoice::factory()->paid()->for($customer)->create(), the intent is obvious at a glance, and the moment your schema changes you fix the data in one factory instead of a hundred tests.
Further Reading
- Laravel documentation, "Eloquent: Factories" (laravel.com/docs/13.x/eloquent-factories)
- Laravel documentation, "Database Testing" (laravel.com/docs/13.x/database-testing)