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 TransportException
s are handled correctly. To do this, I need the mailer to throw TransportException
s 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?
The order should be correct on your first try.
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.
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 themailer.mailer
service and set it to public in the test environment: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...