Supercharging Symfony Testing with Zenstruck Foundry

· Silas Joisten · 3 minutes to read
Toy factory production line

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 it

  • ObjectFactoy: 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 via Proxy 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.

Get Started with Foundry and Testing today

Transform your Symfony testing workflow today with Foundry. Easily generate test data, automate repetitive tasks, and ensure more reliable tests.

This might also interest you

Fabien Potencier
Elise Hamimi New

SymfonyCon Amsterdam 2025: Our Recap and the Highlights

After an iconic first edition in 2019, SymfonyCon made its big comeback to Amsterdam. From the start, you could feel the energy of a highly anticipated conference: more than 1,200 attendees, 39 nationalities, the biggest Symfony community reunion of the year, great discoveries... and a fun atmosphere. This year was extra special because it was the 20th anniversary of Symfony. SensioLabs was there: we'll tell you all about our experience there!

Read more
PHP 8.5 URI extension
Oskar Stark

PHP 8.5's New URI Extension: A Game-Changer for URL Parsing

PHP 8.5 introduces a powerful new URI extension that modernizes URL handling. With support for both RFC 3986 and WHATWG standards, the new Uri class provides immutable objects, fluent interfaces, and proper validation - addressing all the limitations of the legacy parse_url() function. This guide shows practical before/after examples and explains when to use each standard.

Read more
Two images: on the left many cars stuck in a traffic jam with the sign "All directions" above, on the right a blue car moving forward alone on the highway with the sign "Service Subscriber" and a Symfony logo above
Steven Renaux

Symfony Lazy Services with Style: Boost DX using Service Subscribers

Boost your Symfony app's performance and developer experience! Learn how to use Service Subscribers and traits for lazy service loading to reduce eager instantiation, simplify dependencies, and create modular, maintainable code.

Read more
the surface of the earth seen from the space with city lights forming networks
Imen Ezzine

HTTP Verbs: Your Ultimate Guide

HTTP Verbs Explained: Learn the basics of GET, POST, PUT, DELETE, and more. This article explains how they work, their applications, and their security implications.

Read more
Domain Driven Design practical approach
Silas Joisten

Applying Domain-Driven Design in PHP and Symfony: A Hands-On Guide

Learn how to apply Domain-Driven Design (DDD) principles in Symfony with practical examples. Discover the power of value objects, repositories, and bounded contexts.

Read more
type-safety-uuid
Oskar Stark

Type-Safe Identifiers with Symfony and Doctrine: Using Dedicated ID Classes

Learn how to enhance type safety in Symfony and Doctrine by using dedicated ID classes like BookId and UserId instead of raw UUIDs. This approach prevents identifier mix-ups, improves code clarity, and ensures better integration with Symfony Messenger and repository methods. Explore practical examples and best practices for implementing type-safe identifiers in your Symfony applications.

Read more
Grey Cargo Plane with a Blue Sky
Rémi Brière

Agility and the Cargo Cult - Part 1

Agility is more than just rituals and tools. In this first article of our Scrum series, we explore the Cargo Cult phenomenon and how blind imitation can hinder true Agile transformation.

Read more
SemVer vs. CalVer
Silas Joisten

SemVer vs. CalVer: Which Versioning Strategy is Right for You?

SemVer ensures stability for libraries, while CalVer aligns projects with release cycles. Learn the key differences and best use cases to optimize your versioning strategy.

Read more
Image