Supercharging Symfony Testing with Zenstruck Foundry

Zenstruck Foundry has revolutionized the way we write tests in Symfony. In this post, you’ll learn how expressive factories, isolated test data, and a smoother developer experience helped us streamline our testing workflow and boost productivity.
What is Zenstruck Foundry?
Zenstruck Foundry is a powerful library designed to simplify the creation of test data in Symfony applications. At its core, Foundry provides expressive, auto-completable factories for Doctrine entities, enabling developers to quickly generate data with minimal boilerplate.
With support for Faker, integration with Doctrine ORM/ODM, and features like stories, it significantly boosts developer productivity and test reliability.
Creating a Factory for a Doctrine Entity
Imagine we have a User
entity:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string')]
private string $email;
#[ORM\Column(type: 'string')]
private string $name;
}
We can now generate a factory for it:
bin/console make:factory App\\Entity\\User
This command creates the following UserFactory
class.
// src/Factory/UserFactory.php
namespace App\Factory;
use App\Entity\User;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
use Zenstruck\Foundry\Proxy;
/**
* @extends PersistentObjectFactory<User>
*/
final class UserFactory extends PersistentObjectFactory
{
protected function defaults(): array
{
return [
'email' => self::faker()->email(),
'name' => self::faker()->name(),
];
}
protected static function class(): string
{
return User::class;
}
}
Using Factories in Tests
With the factory created, you can now use it to generate users in your test cases:
public function testGetEmail(): void
{
$user = UserFactory::createOne([
'email' => $expected = 'john@doe.com'
]);
self::assertSame($expected, $user->getEmail());
}
Need more users?
$users = UserFactory::createMany(5);
Introducing Stories
Stories are a great way to define reusable data scenarios. For example, if you want a default admin user to always exist, you can define it in a story:
// src/Story/DefaultUsersStory.php
namespace App\Story;
use App\Factory\UserFactory;
use Zenstruck\Foundry\Story;
final class DefaultUsersStory extends Story
{
public function build(): void
{
UserFactory::createOne([
'email' => 'admin@example.com',
'name' => 'Administrator',
]);
}
}
You can load this story using Doctrine fixtures:
bin/console doctrine:fixtures:load
Make sure you register Foundry’s loader in your FoundryBundle configuration if needed.
Built-in Factories
Foundry also includes specialized factories for advanced use cases:
ArrayFactory
: Used to generate arrays of data for instance you can fake API responses with itObjectFactoy
: Used to create objects without persisting them.PersistentProxyObjectFactory
: This one is useful when you want the created objects to be saved immediately to the database and available viaProxy
object.
Testing Value Objects and Aggregates in DDD
One of the standout features of Zenstruck Foundry is its ability to work seamlessly with complex domain models, including Value Objects and Aggregates in Domain-Driven Design (DDD). Foundry simplifies the creation of both entities and their related value objects, making it a perfect fit for testing sophisticated DDD architectures.
➡ Read more about the basics of Domain-Driven Design
For instance, consider testing a value object like a Price
. By utilizing Foundry’s factories, you can quickly create and inject realistic data into the Price
, ensuring your domain logic behaves as expected.
Here’s an example where PriceFactory
creates a Price
value object:
final readonly class Price
{
public function __construct(
public int $amount,
public string $currency,
) {
}
}
use Zenstruck\Foundry\Object\Instantiator\ObjectFactory;
final class PriceFactory extends ObjectFactory
{
protected function defaults(): array
{
return [
'amount' => self::faker()->numberBetween(100, 1000),
'currency' => self::faker()->randomElement(['€', '$']),
];
}
protected static function getClass(): string
{
return Price::class;
}
}
This allows you to test the behavior of aggregates in DDD without worrying about the complexities of creating and managing the underlying data. Foundry helps you simulate realistic domain models, ensuring that aggregates and value objects interact in a way that mirrors real-world scenarios.
Final Thoughts
Zenstruck Foundry has become an essential tool in our Symfony toolbox. It has reduced boilerplate in our tests, improved reliability, and boosted productivity. The ability to use stories for reusable scenarios and advanced factories for complex needs makes it a must-have for any Symfony project.
Additionally, its compatibility with DDD principles allows for seamless testing of value objects and aggregates, making it an excellent choice for complex domain models.