zenstruck/foundry
Zenstruck Foundry is a Laravel-friendly factory and fixtures toolkit for building and persisting test data. Define model factories, create realistic related objects, seed databases, and write cleaner, faster tests with helpers for states, Faker, and repositories.
Installation Run:
composer require --dev zenstruck/foundry
Add to composer.json under require-dev to ensure it’s excluded from production.
Basic Setup Publish the config (optional but recommended for customization):
php artisan vendor:publish --provider="Zenstruck\Foundry\FoundryServiceProvider"
This generates config/foundry.php.
First Fixture
Create a fixture for a model (e.g., User):
php artisan make:foundry User
This generates database/foundry/UserFixture.php with a default state.
Create and Persist
Use the create() method in tests or Tinker:
use Zenstruck\Foundry\ModelFactory;
$user = UserFixture::new()->create();
// or persist to DB:
$user = UserFixture::new()->createPersisted();
State-Based Fixtures Define reusable states in the fixture class:
class UserFixture extends ModelFactory
{
protected function definition(): array
{
return [
'name' => $this->faker->name,
'email' => $this->faker->unique()->email,
];
}
public function admin(): static
{
return $this->state([
'role' => 'admin',
'is_active' => true,
]);
}
}
Usage:
$admin = UserFixture::new()->admin()->createPersisted();
Relationships
Use has() or hasMany() to define relationships:
class PostFixture extends ModelFactory
{
protected function definition(): array
{
return [
'title' => $this->faker->sentence,
'user_id' => UserFixture::new()->create()->id,
];
}
public function withAuthor(): static
{
return $this->has('author', UserFixture::new()->admin());
}
}
Collections Create multiple records at once:
$users = UserFixture::new()->many(5)->create();
// or persisted:
$users = UserFixture::new()->many(3)->createPersisted();
Customization via Callbacks
Use afterCreating() for post-creation logic:
public function withProfile(): static
{
return $this->afterCreating(function (User $user) {
ProfileFixture::new()->create(['user_id' => $user->id]);
});
}
Laravel Testing
Replace create() with createPersisted() in tests to ensure DB interactions:
public function test_user_creation()
{
$user = UserFixture::new()->createPersisted();
$this->assertDatabaseHas('users', ['email' => $user->email]);
}
Seeding
Use fixtures in DatabaseSeeder for consistent test data:
public function run()
{
UserFixture::new()->many(10)->createPersisted();
PostFixture::new()->many(5)->createPersisted();
}
Dynamic Data Pass custom attributes to override defaults:
$user = UserFixture::new()->create([
'name' => 'John Doe',
'email' => 'john@example.com',
]);
Mocking External Services
Use afterCreating() to mock API calls or queue jobs:
$this->afterCreating(function (User $user) {
Mockery::mock('overload', App\Services\Analytics::class)
->shouldReceive('track')
->once()
->with($user->id);
});
Unit Testing Without Doctrine Events In unit tests, disable doctrine events (e.g., observers) for performance:
use Zenstruck\Foundry\TestCase;
class UserTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
$this->withoutDoctrineEvents(); // Disables events for the test
}
public function test_something()
{
$user = UserFixture::new()->create();
// Test logic...
}
}
State Overrides
States are not additive—each call to state() or *() (e.g., admin()) replaces previous states. Chain them carefully:
// Wrong: 'role' will be 'user' (overridden)
UserFixture::new()->admin()->user()->create();
// Correct: Explicitly merge states
UserFixture::new()->state(['role' => 'admin'])->user()->create();
Persisted vs. In-Memory
create() returns an in-memory model (not persisted to DB).createPersisted() triggers events (e.g., created) and validates.refresh() to reload a persisted model from DB:
$user = UserFixture::new()->createPersisted();
$user->refresh()->load('profile'); // Reload with relations
Faker Conflicts If using multiple Faker locales, ensure consistency:
// Set locale globally in config/foundry.php
'faker_locale' => 'en_US',
Or override per fixture:
public function __construct()
{
$this->faker = Faker::create('fr_FR');
}
Circular Relationships
Avoid infinite loops with recursive has() calls. Use afterCreating() for complex setups:
// Bad: Circular dependency
class CategoryFixture extends ModelFactory {
public function withParent(): static {
return $this->has('parent', CategoryFixture::new());
}
}
// Good: Break cycles with callbacks
$category = CategoryFixture::new()->createPersisted();
$parent = CategoryFixture::new()->createPersisted();
$category->parent()->associate($parent);
Doctrine Events in Unit Tests
The withoutDoctrineEvents() method now correctly does nothing in unit tests (previously, it might have caused unexpected behavior). Ensure you’re using it intentionally in integration tests:
// Only use in integration tests, not unit tests
$this->withoutDoctrineEvents();
Inspect States Dump the final state before creation:
$fixture = UserFixture::new()->admin()->withProfile();
dd($fixture->getState());
Check Events Listen for fixture events in tests:
Foundry::on('creating', function ($fixture) {
dump($fixture->getState());
});
Clear Cached Factories If changes to fixtures aren’t reflected, clear the cache:
php artisan cache:clear
php artisan config:clear
Custom Factory Classes
Extend ModelFactory for shared logic:
class BaseUserFixture extends ModelFactory
{
protected function commonDefinition(): array
{
return ['is_active' => true];
}
protected function definition(): array
{
return array_merge($this->commonDefinition(), [
'name' => $this->faker->name,
]);
}
}
Dynamic Attributes
Use afterInstantiated() to compute attributes:
public function withHashedPassword(): static
{
return $this->afterInstantiated(function (User $user) {
$user->password = bcrypt('password');
});
}
Database-Specific Logic
Override persist() for custom DB logic (e.g., UUIDs):
protected function persist(Model $model): Model
{
$model->id = Str::uuid()->toString();
$model->saveOrFail();
return $model;
}
Testing Helpers Create a trait for reusable test setups:
trait CreatesUsers
{
protected function createTestUser(): User
{
return UserFixture::new()->createPersisted();
}
}
How can I help you explore Laravel packages today?