I am using the Symfony mailer in a custom class in a Symfony 6 project. I am using autowiring through type hinting in the class's constructor, like so:
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,
];
}
}
}
The sendEmail()
method is called in a controller and everything works fine.
Now I want to test that TransportException
s are handled correctly. For that I need the mailer to throw TransportException
s in my tests. However, that does not work as I had hoped.
Note: I cannot induce an exception by passing an invalid email address, as the sendMail
method will only allow valid email addresses.
Things I tried:
1) Use mock Mailer
// boot kernel and get Class from container
$container = self::getContainer();
$myClass = $container->get('App\Model\MyClass');
// create mock mailer service
$mailer = $this->createMock(Mailer::class);
$mailer->method('send')
->willThrowException(new TransportException());
$container->set('Symfony\Component\Mailer\Mailer', $mailer);
Turns out I cannot mock the Mailer
class, as it is final
.
2) Use mock (or stub) MailerInterface
// create mock mailer service
$mailer = $this->createStub(MailerInterface::class);
$mailer->method('send')
->willThrowException(new TransportException());
$container->set('Symfony\Component\Mailer\Mailer', $mailer);
No error, but does not throw an exception. It seems the mailer service is not being replaced.
3) Use custom MailerExceptionTester class
// MailerExceptionTester.php
<?php
namespace App\Tests;
use Symfony\Component\Mailer\Envelope;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\RawMessage;
/**
* Always throws a TransportException
*/
final class MailerExceptionTester implements MailerInterface
{
public function send(RawMessage $message, Envelope $envelope = null): void
{
throw new TransportException();
}
}
And in the test:
// create mock mailer service
$mailer = new MailerExceptionTester();
$container->set('Symfony\Component\Mailer\Mailer', $mailer);
Same result as in 2)
4) Try to replace the MailerInterface service instead of Mailer
// create mock mailer service
$mailer = $this->createMock(MailerInterface::class);
$mailer->method('send')
->willThrowException(new TransportException());
$container->set('Symfony\Component\Mailer\MailerInterface', $mailer);
Error message: Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "Symfony\Component\Mailer\MailerInterface" service is private, you cannot replace it.
5) Set MailerInterface to public
// services.yaml
services:
Symfony\Component\Mailer\MailerInterface:
public: true
Error: Cannot instantiate interface Symfony\Component\Mailer\MailerInterface
6) Add alias for MailerInterface
// services.yaml
services:
app.mailer:
alias: Symfony\Component\Mailer\MailerInterface
public: true
Error message: Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "Symfony\Component\Mailer\MailerInterface" service is private, you cannot replace it.
How can I replace the autowired MailerInterface
service in my test?