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?
I was trying to do exactly this, and I believe I have found a solution based off of what you have already tried.
In my services.yaml
I am redeclaring the mailer.mailer
service and setting it as public when 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 in the exact same way as before, however because it is now public we can overwrite which class it uses in the container if we need.
I copied the custom Mailer class you wrote...
// 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 my test code I get the test container and replace the mailer.mailer
service with an instance of the exception throwing class:
$mailer = new MailerExceptionTester();
static::getContainer()->set('mailer.mailer', $mailer);
Now wherever the Mailer service is injected, the class used will be the custom exception throwing class!