Search code examples
symfonydoctrine-ormphpunitfixtures

doctrine flush updates don't sync to Factory-created object in KernelTestCase


I have a test for a service method NewsletterService::unsubscribe. I am trying to test it in testUnsubscribesNewsletterSubscriber() by creating an object NewsletterSubscriber with a Foundry Factory and then calling this unsubscribe method, and then assert that the NewsletterSubscriber was correctly updated.

Service that is being tested:

// NewsletterService.php
class NewsletterService
{
    public function __construct(
        private readonly EntityManagerInterface           $doctrine,
        private readonly LoggerInterface                  $logger,
        private readonly MailerService                    $mailer,
        private readonly NewsletterSubscriptionRepository $repo,
        private readonly CustomerRepository               $customerRepo
    ) { }
// ...
    public function unsubscribe(string $email): void
    {
        $existing = $this->customerRepo->findOneNewsletterSubscriberByEmail($email);
        if (!$existing) {
            $this->logger->warning('Tried to unsubscribe non-existent subscriber', ['email' => $email]);
            return;
        }

        if (!$existing->isSubscribedToNewsletter()) {
            $this->logger->info('Tried to unsubscribe a non-subscriber', ['email' => $email]);
            return;
        }

        $existing->getNewsletterSubscription()->setUnsubscribedAt(new DateTimeImmutable);
        $this->doctrine->persist($existing);
        $this->doctrine->flush();
    }
}

Test:

// NewsletterServiceTest.php
class NewsletterServiceTest extends KernelTestCase
{
    public function testUnsubscribesNewsletterSubscriber(): void
    {
        /** @var Proxy|NewsletterSubscriber $sub */
        $sub = NewsletterSubscriberFixtures::new()->create(
            [
                'email' => 'test@example.com',
                'zipcode' => '12345',
                'newsletterSubscription' => NewsletterSubscriptionFixtures::createOne([
                    'subscribedAt' => new DateTimeImmutable('-1 day'),
                    'unsubscribedAt' => null,
                ]),
            ]
        );

        $this->assertTrue($sub->isSubscribedToNewsletter());

        /** @var NewsletterService $service */
        $service = static::getContainer()->get(NewsletterService::class);
        $service->unsubscribe('test@example.com');

        // I have auto refresh on, but just to be clear:
        $sub->refresh();
        $this->assertNotTrue($sub->isSubscribedToNewsletter()); // fails
    }
}

As you can see, the unsubscribe method finds the NewsletterSubscriber from the repository. (Note: it does find it, I debugged that.) It then sets the unsubscribedAt property. After the unsubscribe call the $sub still has unsubscribedAt = null, which shouldn't be the case as it was set by NewsletterService::unsubscribe and flushed to database.

If I write $em = static::getContainer()->get(EntityManagerInterface::class), I can see in the debugger that unsubscribedAt is set in $em->unitOfWork, but not in $sub->object. I think I can therefore say that this isn't a problem regarding the business logic.

Further digging:

If I add a setUp method like below

    protected function setUp(): void
    {
        \Zenstruck\Foundry\Test\TestState::bootFoundry((new Configuration())
            ->setManagerRegistry(
                static::getContainer()->get(ManagerRegistry::class)
            )
        );
    }

it works. The issue here is that my default locale in config/packages/test/zenstruck_foundry.yaml isn't loaded, which I need in my factories. Also, the above setUp doesn't seem to be the intended way of doing things.


Solution

  • I forgot to add

    // ...
    use Zenstruck\Foundry\Test\Factories;
    // ...
    
    class NewsletterService extends KernelTestCase
    {
        use Factories;
        // ...
    }
    

    as per the documentation. This trait has a setUp function for test-cases involving the kernel (such as KernelTestCase and WebTestCase).