zenstruck / foundry

A model factory library for creating expressive, auto-completable, on-demand dev/test fixtures with Symfony and Doctrine.
https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html
MIT License
609 stars 63 forks source link

Publishing of Messenger events after Global State setup #519

Closed benr77 closed 7 months ago

benr77 commented 7 months ago

Hi Kevin. I've spent the last few days implementing Foundry on a big project, and really getting to grips with all it's wonderful features. I completely love it, so thank you for all your efforts with the package.

One thing I'm a bit stuck on is publishing my domain events that are created in my Global Story.

The application works as follows:

1) Domain events are recorded when an entity is created or modified. 2) These events are then persisted in the same transaction as the entity. 3) There is a listener in kernel.terminate which publishes the events to the message bus. 4) In some cases, an event handler then creates a secondary entity, and persists it.

When Global Story is executed and the main entities are persisted, the events are also recorded and persisted.

However, this means when my test subsequently executes, because the events have not yet been published, these secondary entities have not yet been created .

If I then create the secondary entity manually inside the test using a Foundry Factory, it works. However, when the event is finally published (e.g. during a secondary browse request) it complains that an entity with the given ID already exists.

My current solution is messy and makes the test slower - I add an extra request to the test before visiting the route under test:

$this->browser()
    ->visitRoute('homepage') // To trigger domain event publishing
    ->visitRoute('route_under_test')
;

Can you suggest an appropriate method to handle this situation? I think maybe some method of rebooting the kernel after Global Story but before any tests are executed might be the way to go, but this might well confuse Foundry itself. Or would it maybe be better to have a second Global Story service that actively publishes the events without waiting for kernel.terminate?

A lot of my entities are created based on domain events from another entity, so having all this work seamlessly like it does in the production code would be much more elegant.

Thanks!

nikophil commented 7 months ago

Hi,

I think you can use an invokable service as a global state for this, it would be the last global state used, and will trigger those events:

    zenstruck_foundry:
        global_state:
            - App\Tests\YourGlobalStory
            - App\Tests\MaybeAnotherGlobalStory
            - ...

            # you can add here a service name (which must be invokable)
            # and it should do the same stuff than your listener for `kernel.terminate` does
            - some.invokable.global.state
benr77 commented 7 months ago

Yes, this is the solution I have gone for and it's now working well. Much more elegant!

I have created an invokable service, and then refer to it in the global_state configuration as the last entry.

final readonly class DomainEventFixturePublisher
{
    public function __construct(
        private DomainEventPublisher $publisher
    ) {
    }

    public function __invoke(): void
    {
        echo "Publishing domain events... ";

        $this->publisher->publish();

        echo "done\n";
    }
}