The rewritten title is: How can I simulate the Symfony mailer for testing purposes?
P粉739706089
P粉739706089 2023-12-13 09:42:22
0
2
475

I'm using the Symfony mailer in a custom class in a Symfony 6 project. I'm using autowiring via type hints in the class's constructor like this:

class MyClass {
        public function __construct(private readonly MailerInterface $mailer) {}


        public function sendEmail(): array
        {
            // Email is sent down here
            try {
                $this->mailer->send($email);
            
                return [
                    'success' => true,
                    'message' => 'Email sent',
                ];
            } catch (TransportExceptionInterface $e) {
                return [
                    'success' => false,
                    'message' => 'Error sending email: ' . $e,
                ];
            }
        }
    }

Call the sendEmail() method in the controller and everything is fine.

Now I want to test whether TransportExceptions are handled correctly. To do this, I need the mailer to throw TransportExceptions in my tests. However, this didn't work as I hoped.

Note: I cannot throw an exception by passing an invalid email address because the sendMail method only allows valid email addresses.

Things I've tried:

1) Use simulated email program

// boot kernel and get Class from container
$container = self::getContainer();
$myClass = $container->get('AppModelMyClass');

// create mock mailer service
$mailer = $this->createMock(Mailer::class);
$mailer->method('send')
        ->willThrowException(new TransportException());
$container->set('SymfonyComponentMailerMailer', $mailer);

As a result I cannot mock the Mailer class because it is final.

2) Use a mock (or stub) MailerInterface

// create mock mailer service
$mailer = $this->createStub(MailerInterface::class);
$mailer->method('send')
        ->willThrowException(new TransportException());
$container->set('SymfonyComponentMailerMailer', $mailer);

No errors, but no exceptions are thrown. The mail service does not appear to have been replaced.

3) Use a custom MailerExceptionTester class

// MailerExceptionTester.php
<?php

namespace AppTests;

use SymfonyComponentMailerEnvelope;
use SymfonyComponentMailerExceptionTransportException;
use SymfonyComponentMailerMailerInterface;
use SymfonyComponentMimeRawMessage;

/**
 * Always throws a TransportException
 */
final class MailerExceptionTester implements MailerInterface
{
    public function send(RawMessage $message, Envelope $envelope = null): void
    {
        throw new TransportException();
    }
}

In testing:

// create mock mailer service
$mailer = new MailerExceptionTester();
$container->set('SymfonyComponentMailerMailer', $mailer);

Same result as in 2)

4) Try changing the MailerInterface service instead of Mailer

// create mock mailer service
$mailer = $this->createMock(MailerInterface::class);
$mailer->method('send')
        ->willThrowException(new TransportException());
$container->set('SymfonyComponentMailerMailerInterface', $mailer);

Error message: SymfonyComponentDependencyInjectionExceptionInvalidArgumentException: The 'SymfonyComponentMailerMailerInterface' service is private and you cannot replace it.

5) Set MailerInterface to public

// services.yaml
services:
    SymfonyComponentMailerMailerInterface:
        public: true

Error: Unable to instantiate interface SymfonyComponentMailerMailerInterface

6) Add an alias for MailerInterface

// services.yaml
services:
    app.mailer:
        alias: SymfonyComponentMailerMailerInterface
        public: true

Error message: SymfonyComponentDependencyInjectionExceptionInvalidArgumentException: The 'SymfonyComponentMailerMailerInterface' service is private and you cannot replace it.

How to replace the auto-connected MailerInterface service in a test?

P粉739706089
P粉739706089

reply all(2)
P粉704066087

The order should be correct on your first try.

// boot kernel and get Class from container
$container = self::getContainer();
$container->set('Symfony\Component\Mailer\Mailer', $mailer);

// create mock mailer service
$mailer = $this->createMock(Mailer::class);
$mailer->method('send')
        ->willThrowException(new TransportException());

$myClass = $container->get('App\Model\MyClass');

Not tested, but you are getting the class as an object, so dependencies on the service are already resolved before mocking. This should first replace the service in the container and then get MyClass from the container.

However, you can also skip building the container entirely. Just use PhpUnit.

$mock = $this->createMock(Mailer::class);
// ... 

$myClass = new MyClass($mock);
$myClass->sendEmail();
P粉226667290

I'm trying to do this, and I believe I've found a solution based on what you've already tried.

In my services.yaml I redeclare the mailer.mailer service and set it to public in the test environment:

when@test:
    services:
        mailer.mailer:
            class: Symfony\Component\Mailer\Mailer
            public: true
            arguments:
                - '@mailer.default_transport'

This setup should make the Symfony Mailer service behave exactly the same way as before, but because it is now public we can override the classes it uses in the container if needed.

I copied the custom Mailer class you wrote...

// MailerExceptionTester.php

...In my test code, I took the test container and replaced the mailer.mailer service with an instance of the exception throwing class:

$mailer = new MailerExceptionTester();
static::getContainer()->set('mailer.mailer', $mailer);

Now, whenever you inject the Mailer service, the class used will be a custom exception throwing class!

Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!