PHP Test Data Management: Fixtures, Seeders, and Factories
Test data management is one of the most neglected parts of a test suite. Tests that rely on a specific database state are brittle. Tests that build every object from scratch are verbose and hard to read. Getting it right — using the right tool for each scenario — makes your tests both reliable and easy to maintain.
This guide covers the three main approaches in the PHP ecosystem: Laravel model factories, Doctrine fixtures, and Nelmio Alice fixture files.
Laravel Model Factories
Laravel's model factories are the most ergonomic test data tool in PHP. They let you define default attributes for a model once, then override only what matters per test.
Defining a factory:
// database/factories/UserFactory.php
class UserFactory extends Factory
{
public function definition(): array
{
return [
'name' => $this->faker->name(),
'email' => $this->faker->unique()->safeEmail(),
'password' => bcrypt('password'),
'role' => 'user',
'email_verified_at' => now(),
];
}
public function admin(): static
{
return $this->state(['role' => 'admin']);
}
public function unverified(): static
{
return $this->state(['email_verified_at' => null]);
}
}Using factories in tests:
// Create a single user with defaults
$user = User::factory()->create();
// Create an admin
$admin = User::factory()->admin()->create();
// Override specific attributes
$user = User::factory()->create(['email' => 'alice@example.com']);
// Create without persisting to DB
$user = User::factory()->make();
// Create many
$users = User::factory()->count(5)->create();Relationships with factories:
class OrderFactory extends Factory
{
public function definition(): array
{
return [
'user_id' => User::factory(),
'status' => 'pending',
'total_cents' => $this->faker->numberBetween(999, 99999),
];
}
}
// Creates user + order in one call
$order = Order::factory()->create();
// Create order for existing user
$order = Order::factory()->for($user)->create();
// Create order with line items
$order = Order::factory()
->has(LineItem::factory()->count(3))
->create();The key principle: create only what your test cares about. If you're testing order total calculation, create the order and line items explicitly. Let the factory fill everything else with valid defaults.
Sequences for Varied Data
When you need controlled variation across multiple records:
$users = User::factory()
->count(3)
->sequence(
['role' => 'admin'],
['role' => 'editor'],
['role' => 'viewer'],
)
->create();Or use a closure for dynamic values:
$products = Product::factory()
->count(5)
->sequence(fn ($sequence) => ['sku' => 'SKU-' . str_pad($sequence->index + 1, 4, '0', STR_PAD_LEFT)])
->create();Doctrine Fixtures
In Symfony and other Doctrine-based projects, doctrine/data-fixtures is the standard approach. Fixtures are PHP classes that load data in a defined order.
Install:
composer require --dev doctrine/data-fixturesWriting a fixture:
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
class UserFixture extends AbstractFixture implements OrderedFixtureInterface
{
public function load(ObjectManager $manager): void
{
$user = new User();
$user->setEmail('alice@example.com');
$user->setName('Alice');
$user->setRole('admin');
$manager->persist($user);
$manager->flush();
// Store reference for other fixtures to use
$this->addReference('user-alice', $user);
}
public function getOrder(): int
{
return 1; // load before fixtures that depend on users
}
}
class OrderFixture extends AbstractFixture implements OrderedFixtureInterface
{
public function load(ObjectManager $manager): void
{
/** @var User $user */
$user = $this->getReference('user-alice');
$order = new Order();
$order->setUser($user);
$order->setStatus('completed');
$order->setTotalCents(4999);
$manager->persist($order);
$manager->flush();
}
public function getOrder(): int
{
return 2;
}
}Loading fixtures in a Symfony test:
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class OrderServiceTest extends KernelTestCase
{
protected function setUp(): void
{
self::bootKernel();
$em = self::getContainer()->get('doctrine')->getManager();
$loader = new \Doctrine\Common\DataFixtures\Loader();
$loader->addFixture(new UserFixture());
$loader->addFixture(new OrderFixture());
$executor = new ORMExecutor($em, new ORMPurger());
$executor->execute($loader->getFixtures());
}
}The ORMPurger truncates all tables before loading, giving you a clean slate every test run.
Nelmio Alice — YAML Fixture Files
For large datasets, Doctrine fixture PHP classes become unwieldy. Nelmio Alice lets you define fixtures in YAML with a powerful templating syntax:
Install:
composer require --dev nelmio/alice hautelook/alice-bundleYAML fixture file (fixtures/users.yaml):
App\Entity\User:
user_admin:
name: Alice Admin
email: alice@example.com
role: admin
password: <{hashed_password}>
emailVerifiedAt: <dateTimeBetween('-1 year', 'now')>
user_{1..10}:
name: <name()>
email: <email()>
role: user
password: <{hashed_password}>
emailVerifiedAt: <dateTimeBetween('-6 months', 'now')>
App\Entity\Product:
product_{1..5}:
name: <words(3, true)>
priceCents: <numberBetween(999, 19999)>
stock: <numberBetween(0, 100)>
createdAt: <dateTimeBetween('-1 year', 'now')>
App\Entity\Order:
order_{1..20}:
user: @user_<numberBetween(1, 10)>
status: <randomElement(['pending', 'completed', 'cancelled'])>
totalCents: <numberBetween(999, 99999)>
createdAt: <dateTimeBetween('-3 months', 'now')>Alice generates all entities, resolves the @user_N references automatically, and loads them in dependency order. The {1..10} range syntax generates 10 users in one declaration.
Loading Alice fixtures in tests:
use Nelmio\Alice\Loader\NativeLoader;
$loader = new NativeLoader();
$objectSet = $loader->loadFile(__DIR__ . '/fixtures/users.yaml');
$objects = $objectSet->getObjects();
foreach ($objects as $object) {
$em->persist($object);
}
$em->flush();Choosing the Right Tool
| Scenario | Best tool |
|---|---|
| Laravel project, any size | Model factories |
| Symfony/Doctrine, small datasets | Doctrine fixtures |
| Symfony/Doctrine, large or complex datasets | Nelmio Alice |
| Seeding production-like volumes | Alice or raw SQL seeders |
| One-off test-specific data | Factory with overrides |
The golden rule: your tests should express intent, not construction. User::factory()->admin()->create() reads as a sentence. Ten lines of new User(); $user->set... reads as boilerplate. Invest in your factories and fixture files — it pays dividends every time you write a new test.
Well-managed test data is the foundation of a reliable test suite. Pair it with continuous test monitoring from HelpMeTest to catch regressions the moment they appear in production flows — sign up free for 24/7 test monitoring.