Need an expert to help you on your Symfony or PHP development project? Contact us and get a quote


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.

Image